prosemirror integration
This commit is contained in:
commit
0521757b8c
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.tern-project
|
.tern-project
|
||||||
|
yarn-error.log
|
||||||
|
12
package.json
12
package.json
@ -105,6 +105,18 @@
|
|||||||
"normalize.css": "^4.2.0",
|
"normalize.css": "^4.2.0",
|
||||||
"pluralize": "^3.0.0",
|
"pluralize": "^3.0.0",
|
||||||
"prismjs": "^1.5.1",
|
"prismjs": "^1.5.1",
|
||||||
|
"prosemirror-commands": "^0.12.0",
|
||||||
|
"prosemirror-history": "^0.12.0",
|
||||||
|
"prosemirror-inputrules": "^0.12.0",
|
||||||
|
"prosemirror-keymap": "^0.12.0",
|
||||||
|
"prosemirror-markdown": "^0.12.0",
|
||||||
|
"prosemirror-model": "^0.12.0",
|
||||||
|
"prosemirror-schema-basic": "^0.12.0",
|
||||||
|
"prosemirror-schema-list": "^0.12.0",
|
||||||
|
"prosemirror-schema-table": "^0.12.0",
|
||||||
|
"prosemirror-state": "^0.12.0",
|
||||||
|
"prosemirror-transform": "^0.12.1",
|
||||||
|
"prosemirror-view": "^0.12.0",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-addons-css-transition-group": "^15.3.1",
|
"react-addons-css-transition-group": "^15.3.1",
|
||||||
"react-datetime": "^2.6.0",
|
"react-datetime": "^2.6.0",
|
||||||
|
@ -5,7 +5,7 @@ const availableIcons = [
|
|||||||
// Font Awesome Editor Icons
|
// Font Awesome Editor Icons
|
||||||
'bold', 'italic', 'list', 'font', 'text-height', 'text-width', 'align-left', 'align-center', 'align-right',
|
'bold', 'italic', 'list', 'font', 'text-height', 'text-width', 'align-left', 'align-center', 'align-right',
|
||||||
'align-justify', 'indent-left', 'indent-right', 'list-bullet', 'list-numbered', 'strike', 'underline', 'table',
|
'align-justify', 'indent-left', 'indent-right', 'list-bullet', 'list-numbered', 'strike', 'underline', 'table',
|
||||||
'superscript', 'subscript', 'header', 'h1', 'h2', 'paragraph', 'link', 'unlink', 'quote-left', 'quote-right', 'code',
|
'superscript', 'subscript', 'header', 'h1', 'h2', 'h3', 'paragraph', 'link', 'unlink', 'quote-left', 'quote-right', 'code',
|
||||||
'picture', 'video',
|
'picture', 'video',
|
||||||
// Entypo
|
// Entypo
|
||||||
'note', 'note-beamed',
|
'note', 'note-beamed',
|
||||||
@ -183,7 +183,7 @@ const availableIcons = [
|
|||||||
'smashing',
|
'smashing',
|
||||||
'sweden',
|
'sweden',
|
||||||
'db-shape',
|
'db-shape',
|
||||||
'logo-db'
|
'logo-db',
|
||||||
];
|
];
|
||||||
|
|
||||||
const iconPropType = (props, propName) => {
|
const iconPropType = (props, propName) => {
|
||||||
@ -191,16 +191,16 @@ const iconPropType = (props, propName) => {
|
|||||||
const value = props[propName];
|
const value = props[propName];
|
||||||
if (typeof value !== 'string' || availableIcons.indexOf(value) === -1) {
|
if (typeof value !== 'string' || availableIcons.indexOf(value) === -1) {
|
||||||
return new Error(
|
return new Error(
|
||||||
`Invalid type "${value}" supplied to Icon Component.`
|
`Invalid type "${ value }" supplied to Icon Component.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const noop = function() {};
|
const noop = function () {};
|
||||||
|
|
||||||
export default function Icon({ style, className = '', type, onClick = noop }) {
|
export default function Icon({ style, className = '', type, onClick = noop }) {
|
||||||
return <span className={`${styles.root} ${styles[type]} ${className}`} style={style} onClick={onClick} />;
|
return <span className={`${ styles.root } ${ styles[type] } ${ className }`} style={style} onClick={onClick} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
Icon.propTypes = {
|
Icon.propTypes = {
|
||||||
|
@ -16,49 +16,50 @@ class MarkdownControl extends React.Component {
|
|||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { mode: 'visual' };
|
||||||
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.useRawEditor();
|
|
||||||
processEditorPlugins(registry.getEditorComponents());
|
processEditorPlugins(registry.getEditorComponents());
|
||||||
}
|
}
|
||||||
|
|
||||||
useVisualEditor = () => {
|
handleMode = (mode) => {
|
||||||
this.props.switchVisualMode(true);
|
this.setState({ mode });
|
||||||
};
|
|
||||||
|
|
||||||
useRawEditor = () => {
|
|
||||||
this.props.switchVisualMode(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { editor, onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
|
const { onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
|
||||||
if (editor.get('useVisualMode')) {
|
const { mode } = this.state;
|
||||||
|
if (mode === 'visual') {
|
||||||
return (
|
return (
|
||||||
<div className="cms-editor-visual">
|
<div className="cms-editor-visual">
|
||||||
{null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>}
|
|
||||||
<VisualEditor
|
<VisualEditor
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onAddMedia={onAddMedia}
|
onAddMedia={onAddMedia}
|
||||||
|
onRemoveMedia={onRemoveMedia}
|
||||||
|
onMode={this.handleMode}
|
||||||
getMedia={getMedia}
|
getMedia={getMedia}
|
||||||
registeredComponents={editor.get('registeredComponents')}
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cms-editor-raw">
|
<div className="cms-editor-raw">
|
||||||
{null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>}
|
|
||||||
<RawEditor
|
<RawEditor
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onAddMedia={onAddMedia}
|
onAddMedia={onAddMedia}
|
||||||
onRemoveMedia={onRemoveMedia}
|
onRemoveMedia={onRemoveMedia}
|
||||||
|
onMode={this.handleMode}
|
||||||
getMedia={getMedia}
|
getMedia={getMedia}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { fromJS } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
import { Button } from 'react-toolbox/lib/button';
|
import { Button } from 'react-toolbox/lib/button';
|
||||||
import { resolveWidget } from '../../../Widgets';
|
import { resolveWidget } from '../../Widgets';
|
||||||
import styles from './BlockMenu.css';
|
import styles from './BlockMenu.css';
|
||||||
|
|
||||||
export default class BlockMenu extends Component {
|
export default class BlockMenu extends Component {
|
||||||
@ -49,7 +49,7 @@ export default class BlockMenu extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buttonFor(plugin) {
|
buttonFor(plugin) {
|
||||||
return (<li key={plugin.get('id')}>
|
return (<li key={`plugin-${ plugin.get('id') }`}>
|
||||||
<button onClick={this.handlePlugin(plugin)}>{plugin.get('label')}</button>
|
<button onClick={this.handlePlugin(plugin)}>{plugin.get('label')}</button>
|
||||||
</li>);
|
</li>);
|
||||||
}
|
}
|
||||||
@ -57,8 +57,7 @@ export default class BlockMenu extends Component {
|
|||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { openPlugin, pluginData } = this.state;
|
const { openPlugin, pluginData } = this.state;
|
||||||
const toBlock = openPlugin.get('toBlock');
|
this.props.onBlock(openPlugin, pluginData);
|
||||||
this.props.onBlock(toBlock.call(toBlock, pluginData.toJS()));
|
|
||||||
this.setState({ openPlugin: null, isExpanded: false });
|
this.setState({ openPlugin: null, isExpanded: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ export default class BlockMenu extends Component {
|
|||||||
const value = pluginData.get(field.get('name'));
|
const value = pluginData.get(field.get('name'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.control} key={field.get('name')}>
|
<div className={styles.control} key={`field-${ field.get('name') }`}>
|
||||||
<label className={styles.label}>{field.get('label')}</label>
|
<label className={styles.label}>{field.get('label')}</label>
|
||||||
{
|
{
|
||||||
React.createElement(widget.control, {
|
React.createElement(widget.control, {
|
@ -1,28 +0,0 @@
|
|||||||
.Toolbar {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
display: none;
|
|
||||||
margin: none;
|
|
||||||
padding: none;
|
|
||||||
box-shadow: 1px 1px 5px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
& button {
|
|
||||||
padding: 5px;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button:last-child button {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
@ -6,8 +6,8 @@ import htmlSyntax from 'markup-it/syntaxes/html';
|
|||||||
import CaretPosition from 'textarea-caret-position';
|
import CaretPosition from 'textarea-caret-position';
|
||||||
import registry from '../../../../lib/registry';
|
import registry from '../../../../lib/registry';
|
||||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from '../Toolbar';
|
||||||
import BlockMenu from './BlockMenu';
|
import BlockMenu from '../BlockMenu';
|
||||||
import styles from './index.css';
|
import styles from './index.css';
|
||||||
|
|
||||||
const HAS_LINE_BREAK = /\n/m;
|
const HAS_LINE_BREAK = /\n/m;
|
||||||
@ -92,9 +92,7 @@ export default class RawEditor extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const plugins = registry.getEditorComponents();
|
const plugins = registry.getEditorComponents();
|
||||||
this.state = {
|
this.state = { plugins };
|
||||||
plugins: plugins,
|
|
||||||
};
|
|
||||||
this.shortcuts = {
|
this.shortcuts = {
|
||||||
meta: {
|
meta: {
|
||||||
b: this.handleBold,
|
b: this.handleBold,
|
||||||
@ -238,15 +236,16 @@ export default class RawEditor extends React.Component {
|
|||||||
const selection = this.getSelection();
|
const selection = this.getSelection();
|
||||||
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
||||||
try {
|
try {
|
||||||
const position = this.caretPosition.get(selection.start, selection.end);
|
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
|
||||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition: position });
|
console.log('pos: %o', selectionPosition);
|
||||||
|
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||||
}
|
}
|
||||||
} else if (selection.start === selection.end) {
|
} else if (selection.start === selection.end) {
|
||||||
const newBlock =
|
const newBlock =
|
||||||
(
|
(
|
||||||
(selection.start === 0 && value.substr(0,1).match(/^\n?$/)) ||
|
(selection.start === 0 && value.substr(0, 1).match(/^\n?$/)) ||
|
||||||
value.substr(selection.start - 2, 2) === '\n\n') &&
|
value.substr(selection.start - 2, 2) === '\n\n') &&
|
||||||
(
|
(
|
||||||
selection.end === (value.length - 1) ||
|
selection.end === (value.length - 1) ||
|
||||||
@ -270,8 +269,9 @@ export default class RawEditor extends React.Component {
|
|||||||
this.updateHeight();
|
this.updateHeight();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBlock = (chars) => {
|
handleBlock = (plugin, data) => {
|
||||||
this.replaceSelection(chars);
|
const toBlock = plugin.get('toBlock');
|
||||||
|
this.replaceSelection(toBlock.call(toBlock, data.toJS()));
|
||||||
this.setState({ showBlockMenu: false });
|
this.setState({ showBlockMenu: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -313,7 +313,11 @@ export default class RawEditor extends React.Component {
|
|||||||
this.newSelection = newSelection;
|
this.newSelection = newSelection;
|
||||||
onChange(beforeSelection + paste + afterSelection);
|
onChange(beforeSelection + paste + afterSelection);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
handleToggle = () => {
|
||||||
|
this.props.onMode('visual');
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onAddMedia, onRemoveMedia, getMedia } = this.props;
|
const { onAddMedia, onRemoveMedia, getMedia } = this.props;
|
||||||
@ -327,6 +331,7 @@ export default class RawEditor extends React.Component {
|
|||||||
onBold={this.handleBold}
|
onBold={this.handleBold}
|
||||||
onItalic={this.handleItalic}
|
onItalic={this.handleItalic}
|
||||||
onLink={this.handleLink}
|
onLink={this.handleLink}
|
||||||
|
onToggleMode={this.handleToggle}
|
||||||
/>
|
/>
|
||||||
<BlockMenu
|
<BlockMenu
|
||||||
isOpen={showBlockMenu}
|
isOpen={showBlockMenu}
|
||||||
@ -353,5 +358,6 @@ RawEditor.propTypes = {
|
|||||||
onRemoveMedia: PropTypes.func.isRequired,
|
onRemoveMedia: PropTypes.func.isRequired,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onMode: PropTypes.func.isRequired,
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
107
src/components/Widgets/MarkdownControlElements/Toolbar.css
Normal file
107
src/components/Widgets/MarkdownControlElements/Toolbar.css
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
.editor {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& h1,
|
||||||
|
& h2,
|
||||||
|
& h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
color: #7c8382;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& div[data-plugin] {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
& .ProseMirror {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-drop-target {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
background: #666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content ul,
|
||||||
|
& .ProseMirror-content ol {
|
||||||
|
padding-left: 30px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content blockquote {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
border-left: 3px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content li {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none; /* Don't do weird stuff with marker clicks */
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content li > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-nodeselection *::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-nodeselection *::-moz-selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-selectednode {
|
||||||
|
outline: 2px solid #8cf;
|
||||||
|
}
|
||||||
|
/* Make sure li selections wrap around markers */
|
||||||
|
& li.ProseMirror-selectednode {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& li.ProseMirror-selectednode:after {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -32px;
|
||||||
|
border: 2px solid #8cf;
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { Icon } from '../../../UI';
|
import { Icon } from '../../UI';
|
||||||
import styles from './Toolbar.css';
|
import styles from './Toolbar.css';
|
||||||
|
|
||||||
function button(label, icon, action) {
|
function button(label, icon, action) {
|
||||||
@ -19,6 +19,7 @@ export default class Toolbar extends Component {
|
|||||||
onBold: PropTypes.func.isRequired,
|
onBold: PropTypes.func.isRequired,
|
||||||
onItalic: PropTypes.func.isRequired,
|
onItalic: PropTypes.func.isRequired,
|
||||||
onLink: PropTypes.func.isRequired,
|
onLink: PropTypes.func.isRequired,
|
||||||
|
onToggleMode: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
@ -41,7 +42,7 @@ export default class Toolbar extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isOpen, onH1, onH2, onBold, onItalic, onLink } = this.props;
|
const { isOpen, onH1, onH2, onBold, onItalic, onLink, onToggleMode } = this.props;
|
||||||
const classNames = [styles.Toolbar];
|
const classNames = [styles.Toolbar];
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -55,6 +56,7 @@ export default class Toolbar extends Component {
|
|||||||
{button('Bold', 'bold', onBold)}
|
{button('Bold', 'bold', onBold)}
|
||||||
{button('Italic', 'italic', onItalic)}
|
{button('Italic', 'italic', onItalic)}
|
||||||
{button('Link', 'link', onLink)}
|
{button('Link', 'link', onLink)}
|
||||||
|
{button('View Code', 'code', onToggleMode)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,71 +0,0 @@
|
|||||||
.root {
|
|
||||||
border: dotted 1px #ddd;
|
|
||||||
position: relative;
|
|
||||||
margin: 9px 0 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.type:after {
|
|
||||||
content: attr(data-type);
|
|
||||||
font-size: 10px;
|
|
||||||
color: #aaa;
|
|
||||||
position: absolute;
|
|
||||||
top : -7px;;
|
|
||||||
margin-left: 1em;
|
|
||||||
padding: 0 3px;
|
|
||||||
display: inline;
|
|
||||||
background-color: #fafafa;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body img{
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Paragraph {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading1, .Heading2, .Heading3, .Heading4, .Heading5, .Heading6 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: bold
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading1 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading2 {
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading3 {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading4 {
|
|
||||||
font-size: 1.07em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading5 {
|
|
||||||
font-size: 1.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Heading6 {
|
|
||||||
font-size: 1.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockquote {
|
|
||||||
padding-left: 5px;
|
|
||||||
border-left: solid 3px #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body ul {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import styles from './Block.css';
|
|
||||||
|
|
||||||
const AVAILABLE_TYPES = [
|
|
||||||
'Paragraph',
|
|
||||||
'Heading1',
|
|
||||||
'Heading2',
|
|
||||||
'Heading3',
|
|
||||||
'Heading4',
|
|
||||||
'Heading5',
|
|
||||||
'Heading6',
|
|
||||||
'List',
|
|
||||||
'blockquote'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Block({ type, children }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<div contentEditable={false} className={styles.type} data-type={type}/>
|
|
||||||
<div className={`${styles.body} ${styles[type]}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Block.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
type: PropTypes.oneOf(AVAILABLE_TYPES).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Block;
|
|
@ -1,32 +0,0 @@
|
|||||||
.root {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-top: 2px;
|
|
||||||
color: #ddd;
|
|
||||||
transition: color 0.5s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.button:hover {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
left: 20px;
|
|
||||||
height: 32px;
|
|
||||||
white-space: nowrap;
|
|
||||||
background-color: rgba(126, 126, 126, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
|
||||||
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
|
|
||||||
import { Icon } from '../../../UI';
|
|
||||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
|
||||||
import styles from './BlockTypesMenu.css';
|
|
||||||
|
|
||||||
class BlockTypesMenu extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
plugins: PropTypes.array.isRequired,
|
|
||||||
onClickBlock: PropTypes.func.isRequired,
|
|
||||||
onClickPlugin: PropTypes.func.isRequired,
|
|
||||||
onClickImage: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
expanded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUpdate() {
|
|
||||||
if (this.state.expanded) {
|
|
||||||
this.setState({ expanded: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMenu = () => {
|
|
||||||
this.setState({ expanded: !this.state.expanded });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlockTypeClick = (e, type) => {
|
|
||||||
this.props.onClickBlock(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePluginClick = (e, plugin) => {
|
|
||||||
const data = {};
|
|
||||||
plugin.fields.forEach((field) => {
|
|
||||||
data[field.name] = window.prompt(field.label); // eslint-disable-line
|
|
||||||
});
|
|
||||||
this.props.onClickPlugin(plugin.id, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFileUploadClick = () => {
|
|
||||||
this._fileInput.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFileUploadChange = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files;
|
|
||||||
const files = [...fileList];
|
|
||||||
const imageType = /^image\//;
|
|
||||||
|
|
||||||
// Iterate through the list of files and return the first image on the list
|
|
||||||
const file = files.find((currentFile) => {
|
|
||||||
if (imageType.test(currentFile.type)) {
|
|
||||||
return currentFile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
const mediaProxy = new MediaProxy(file.name, file);
|
|
||||||
this.props.onClickImage(mediaProxy);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderBlockTypeButton = (type, icon) => {
|
|
||||||
const onClick = e => this.handleBlockTypeClick(e, type);
|
|
||||||
return (
|
|
||||||
<Icon key={type} type={icon} onClick={onClick} className={styles.icon} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderPluginButton = (plugin) => {
|
|
||||||
const onClick = e => this.handlePluginClick(e, plugin);
|
|
||||||
return (
|
|
||||||
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMenu() {
|
|
||||||
const { plugins } = this.props;
|
|
||||||
if (this.state.expanded) {
|
|
||||||
return (
|
|
||||||
<div className={styles.menu}>
|
|
||||||
{this.renderBlockTypeButton('hr', 'dot-3')}
|
|
||||||
{plugins.map(plugin => this.renderPluginButton(plugin))}
|
|
||||||
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={this.handleFileUploadChange}
|
|
||||||
className={styles.input}
|
|
||||||
ref={(el) => {
|
|
||||||
this._fileInput = el;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu} />
|
|
||||||
{this.renderMenu()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withPortalAtCursorPosition(BlockTypesMenu);
|
|
@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
.button {
|
|
||||||
color: #ccc;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button[data-active="true"] {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.menu > * {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu > * + * {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverMenu {
|
|
||||||
padding: 8px 7px 6px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: -10000px;
|
|
||||||
left: -10000px;
|
|
||||||
margin-top: -6px;
|
|
||||||
opacity: 0;
|
|
||||||
background-color: #222;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: opacity .75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverMenu .button {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverMenu .button[data-active="true"] {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
|
||||||
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
|
|
||||||
import { Icon } from '../../../UI';
|
|
||||||
import styles from './StylesMenu.css';
|
|
||||||
|
|
||||||
class StylesMenu extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
marks: PropTypes.object.isRequired,
|
|
||||||
blocks: PropTypes.object.isRequired,
|
|
||||||
inlines: PropTypes.object.isRequired,
|
|
||||||
onClickBlock: PropTypes.func.isRequired,
|
|
||||||
onClickMark: PropTypes.func.isRequired,
|
|
||||||
onClickInline: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to set toolbar buttons to active state
|
|
||||||
*/
|
|
||||||
hasMark = (type) => {
|
|
||||||
const { marks } = this.props;
|
|
||||||
return marks.some(mark => mark.type == type);
|
|
||||||
};
|
|
||||||
|
|
||||||
hasBlock = (type) => {
|
|
||||||
const { blocks } = this.props;
|
|
||||||
return blocks.some(node => node.type == type);
|
|
||||||
};
|
|
||||||
|
|
||||||
hasLinks = (type) => {
|
|
||||||
const { inlines } = this.props;
|
|
||||||
return inlines.some(inline => inline.type == 'link');
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMarkClick = (e, type) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onClickMark(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMarkButton = (type, icon) => {
|
|
||||||
const isActive = this.hasMark(type);
|
|
||||||
const onMouseDown = e => this.handleMarkClick(e, type);
|
|
||||||
return (
|
|
||||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
|
||||||
<Icon type={icon} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleInlineClick = (e, type, isActive) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onClickInline(type, isActive);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderLinkButton = () => {
|
|
||||||
const isActive = this.hasLinks();
|
|
||||||
const onMouseDown = e => this.handleInlineClick(e, 'link', isActive);
|
|
||||||
return (
|
|
||||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
|
||||||
<Icon type="link" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlockClick = (e, type) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isActive = this.hasBlock(type);
|
|
||||||
const isList = this.hasBlock('list-item');
|
|
||||||
this.props.onClickBlock(type, isActive, isList);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderBlockButton = (type, icon, checkType) => {
|
|
||||||
checkType = checkType || type;
|
|
||||||
const isActive = this.hasBlock(checkType);
|
|
||||||
const onMouseDown = e => this.handleBlockClick(e, type);
|
|
||||||
return (
|
|
||||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
|
||||||
<Icon type={icon} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={`${ styles.menu } ${ styles.hoverMenu }`}>
|
|
||||||
{this.renderMarkButton('BOLD', 'bold')}
|
|
||||||
{this.renderMarkButton('ITALIC', 'italic')}
|
|
||||||
{this.renderMarkButton('CODE', 'code')}
|
|
||||||
{this.renderLinkButton()}
|
|
||||||
{this.renderBlockButton('header_one', 'h1')}
|
|
||||||
{this.renderBlockButton('header_two', 'h2')}
|
|
||||||
{this.renderBlockButton('blockquote', 'quote-left')}
|
|
||||||
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withPortalAtCursorPosition(StylesMenu);
|
|
@ -1,26 +1,91 @@
|
|||||||
.active {
|
.editor {
|
||||||
box-shadow: 0 0 0 2px blue;
|
position: relative;
|
||||||
|
& h1, & h2, & h3 {
|
||||||
|
padding: 0;
|
||||||
|
color: #7c8382;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
& h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
& h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
& h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
& p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
& div[data-plugin] {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global .plugin {
|
:global {
|
||||||
background-color: #ddd;
|
& .ProseMirror {
|
||||||
color: #555;
|
position: relative;
|
||||||
text-align: center;
|
}
|
||||||
width: 200px;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global .plugin_icon {
|
& .ProseMirror-content {
|
||||||
font-size: 50px;
|
white-space: pre-wrap;
|
||||||
margin: 12px 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:global .plugin_fields {
|
& .ProseMirror-drop-target {
|
||||||
font-size: 11px;
|
position: absolute;
|
||||||
outline:none;
|
width: 1px;
|
||||||
}
|
background: #666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
:global .active {
|
& .ProseMirror-content ul, & .ProseMirror-content ol {
|
||||||
box-shadow: 0 0 0 2px blue;
|
padding-left: 30px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content blockquote {
|
||||||
|
padding-left: 1em;
|
||||||
|
border-left: 3px solid #eee;
|
||||||
|
margin-left: 0; margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-content li {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none; /* Don't do weird stuff with marker clicks */
|
||||||
|
}
|
||||||
|
& .ProseMirror-content li > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ProseMirror-nodeselection *::selection { background: transparent; }
|
||||||
|
& .ProseMirror-nodeselection *::-moz-selection { background: transparent; }
|
||||||
|
|
||||||
|
& .ProseMirror-selectednode {
|
||||||
|
outline: 2px solid #8cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure li selections wrap around markers */
|
||||||
|
|
||||||
|
& li.ProseMirror-selectednode {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& li.ProseMirror-selectednode:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -32px;
|
||||||
|
right: -2px; top: -2px; bottom: -2px;
|
||||||
|
border: 2px solid #8cf;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,298 +1,256 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import _ from 'lodash';
|
import { Schema } from 'prosemirror-model';
|
||||||
import { Editor, Raw } from 'slate';
|
import { EditorState } from 'prosemirror-state';
|
||||||
import PluginDropImages from 'slate-drop-or-paste-images';
|
import { EditorView } from 'prosemirror-view';
|
||||||
import MarkupIt, { SlateUtils } from 'markup-it';
|
import history from 'prosemirror-history';
|
||||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
import {
|
||||||
import { emptyParagraphBlock, mediaproxyBlock } from '../constants';
|
blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule,
|
||||||
import { DEFAULT_NODE, SCHEMA } from './schema';
|
inputRules, allInputRules,
|
||||||
import { getNodes, getSyntaxes, getPlugins } from '../../richText';
|
} from 'prosemirror-inputrules';
|
||||||
import StylesMenu from './StylesMenu';
|
import { keymap } from 'prosemirror-keymap';
|
||||||
import BlockTypesMenu from './BlockTypesMenu';
|
import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||||
|
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||||
|
import registry from '../../../../lib/registry';
|
||||||
|
import { buildKeymap } from './keymap';
|
||||||
|
import createMarkdownParser from './parser';
|
||||||
|
import Toolbar from '../Toolbar';
|
||||||
|
import BlockMenu from '../BlockMenu';
|
||||||
|
import styles from './index.css';
|
||||||
|
|
||||||
/**
|
function processUrl(url) {
|
||||||
* Slate Render Configuration
|
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||||
*/
|
return url;
|
||||||
export default class VisualEditor extends React.Component {
|
}
|
||||||
|
if (url.match(/^[^\/]+\.[^\/]+/)) {
|
||||||
|
return `https://${ url }`;
|
||||||
|
}
|
||||||
|
return `/${ url }`;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
const ruleset = {
|
||||||
onChange: PropTypes.func.isRequired,
|
blockquote: [blockQuoteRule],
|
||||||
onAddMedia: PropTypes.func.isRequired,
|
ordered_list: [orderedListRule],
|
||||||
getMedia: PropTypes.func.isRequired,
|
bullet_list: [bulletListRule],
|
||||||
value: PropTypes.string,
|
code_block: [codeBlockRule],
|
||||||
|
heading: [headingRule, 6],
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildInputRules(schema) {
|
||||||
|
const result = [];
|
||||||
|
for (const rule in ruleset) {
|
||||||
|
const type = schema.nodes[rule];
|
||||||
|
if (type) {
|
||||||
|
const fn = ruleset[rule];
|
||||||
|
result.push(fn[0].apply(fn.slice(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markActive(state, type) {
|
||||||
|
const { from, to, empty } = state.selection;
|
||||||
|
if (empty) {
|
||||||
|
return type.isInSet(state.storedMarks || state.doc.marksAt(from));
|
||||||
|
}
|
||||||
|
return state.doc.rangeHasMark(from, to, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaWithPlugins(schema, plugins) {
|
||||||
|
let nodeSpec = schema.nodeSpec;
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
const attrs = {};
|
||||||
|
plugin.get('fields').forEach((field) => {
|
||||||
|
attrs[field.get('name')] = { default: null };
|
||||||
|
});
|
||||||
|
nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, {
|
||||||
|
attrs,
|
||||||
|
group: 'block',
|
||||||
|
parseDOM: [{
|
||||||
|
tag: 'div[data-plugin]',
|
||||||
|
getAttrs(dom) {
|
||||||
|
return JSON.parse(dom.getAttribute('data-plugin'));
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
toDOM(node) {
|
||||||
|
return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Schema({
|
||||||
|
nodes: nodeSpec,
|
||||||
|
marks: schema.markSpec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSerializer(schema, plugins) {
|
||||||
|
const serializer = Object.create(defaultMarkdownSerializer);
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => {
|
||||||
|
const toBlock = plugin.get('toBlock');
|
||||||
|
state.write(toBlock.call(plugin, node.attrs));
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
return serializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Editor extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const plugins = registry.getEditorComponents();
|
||||||
const MarkdownSyntax = getSyntaxes(this.getMedia).markdown;
|
const s = schemaWithPlugins(schema, plugins);
|
||||||
this.markdown = new MarkupIt(MarkdownSyntax);
|
|
||||||
|
|
||||||
SCHEMA.nodes = _.merge(SCHEMA.nodes, getNodes());
|
|
||||||
|
|
||||||
this.blockEdit = false;
|
|
||||||
|
|
||||||
let rawJson;
|
|
||||||
if (props.value !== undefined) {
|
|
||||||
const content = this.markdown.toContent(props.value);
|
|
||||||
rawJson = SlateUtils.encode(content, null, ['mediaproxy'].concat(getPlugins().map(plugin => plugin.id)));
|
|
||||||
} else {
|
|
||||||
rawJson = emptyParagraphBlock;
|
|
||||||
}
|
|
||||||
this.state = {
|
this.state = {
|
||||||
state: Raw.deserialize(rawJson, { terse: true }),
|
plugins,
|
||||||
|
schema: s,
|
||||||
|
parser: createMarkdownParser(s, plugins),
|
||||||
|
serializer: createSerializer(s, plugins),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.plugins = [
|
componentDidMount() {
|
||||||
PluginDropImages({
|
const { schema, parser } = this.state;
|
||||||
applyTransform: (transform, file) => {
|
const doc = parser.parse(this.props.value || '');
|
||||||
const mediaProxy = new MediaProxy(file.name, file);
|
this.view = new EditorView(this.ref, {
|
||||||
props.onAddMedia(mediaProxy);
|
state: EditorState.create({
|
||||||
return transform
|
doc,
|
||||||
.insertBlock(mediaproxyBlock(mediaProxy));
|
schema,
|
||||||
},
|
plugins: [
|
||||||
|
inputRules({
|
||||||
|
rules: allInputRules.concat(buildInputRules(schema)),
|
||||||
}),
|
}),
|
||||||
];
|
keymap(buildKeymap(schema)),
|
||||||
}
|
keymap(baseKeymap),
|
||||||
|
history.history(),
|
||||||
getMedia = (src) => {
|
keymap({
|
||||||
return this.props.getMedia(src);
|
'Mod-z': history.undo,
|
||||||
};
|
'Mod-y': history.redo,
|
||||||
|
}),
|
||||||
/**
|
],
|
||||||
* Slate keeps track of selections, scroll position etc.
|
}),
|
||||||
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
|
onAction: this.handleAction,
|
||||||
* It also have an onDocumentChange, that get's dispatched only when the actual
|
|
||||||
* content changes
|
|
||||||
*/
|
|
||||||
handleChange = (state) => {
|
|
||||||
if (this.blockEdit) {
|
|
||||||
this.blockEdit = false;
|
|
||||||
} else {
|
|
||||||
this.setState({ state });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDocumentChange = (document, state) => {
|
|
||||||
const rawJson = Raw.serialize(state, { terse: true });
|
|
||||||
const content = SlateUtils.decode(rawJson);
|
|
||||||
this.props.onChange(this.markdown.toText(content));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle marks / blocks when button is clicked
|
|
||||||
*/
|
|
||||||
handleMarkStyleClick = (type) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.toggleMark(type)
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlockStyleClick = (type, isActive, isList) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
let transform = state.transform();
|
|
||||||
const { document } = state;
|
|
||||||
|
|
||||||
// Handle everything but list buttons.
|
|
||||||
if (type != 'unordered_list' && type != 'ordered_list') {
|
|
||||||
if (isList) {
|
|
||||||
transform = transform
|
|
||||||
.setBlock(isActive ? DEFAULT_NODE : type)
|
|
||||||
.unwrapBlock('unordered_list')
|
|
||||||
.unwrapBlock('ordered_list');
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
transform = transform
|
|
||||||
.setBlock(isActive ? DEFAULT_NODE : type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the extra wrapping required for list buttons.
|
|
||||||
else {
|
|
||||||
const isType = state.blocks.some((block) => {
|
|
||||||
return !!document.getClosest(block, parent => parent.type == type);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isList && isType) {
|
handleAction = (action) => {
|
||||||
transform = transform
|
const { schema, serializer } = this.state;
|
||||||
.setBlock(DEFAULT_NODE)
|
const newState = this.view.state.applyAction(action);
|
||||||
.unwrapBlock('unordered_list');
|
const md = serializer.serialize(newState.doc);
|
||||||
} else if (isList) {
|
this.props.onChange(md);
|
||||||
transform = transform
|
this.view.updateState(newState);
|
||||||
.unwrapBlock(type == 'unordered_list')
|
if (newState.selection !== this.state.selection) {
|
||||||
.wrapBlock(type);
|
this.handleSelection(newState);
|
||||||
|
}
|
||||||
|
this.view.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelection = (state) => {
|
||||||
|
const { schema, selection } = state;
|
||||||
|
if (selection.from === selection.to) {
|
||||||
|
const { $from } = selection;
|
||||||
|
if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') {
|
||||||
|
const pos = this.view.coordsAtPos(selection.from);
|
||||||
|
const editorPos = this.view.content.getBoundingClientRect();
|
||||||
|
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
||||||
|
this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition });
|
||||||
} else {
|
} else {
|
||||||
transform = transform
|
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||||
.setBlock('list_item')
|
|
||||||
.wrapBlock(type);
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
const pos = this.view.coordsAtPos(selection.from);
|
||||||
state = transform.apply();
|
const editorPos = this.view.content.getBoundingClientRect();
|
||||||
this.setState({ state });
|
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
||||||
};
|
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
||||||
|
|
||||||
/**
|
|
||||||
* When clicking a link, if the selection has a link in it, remove the link.
|
|
||||||
* Otherwise, add a new link with an href and text.
|
|
||||||
*
|
|
||||||
* @param {Event} e
|
|
||||||
*/
|
|
||||||
|
|
||||||
handleInlineClick = (type, isActive) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
|
|
||||||
if (type === 'link') {
|
|
||||||
if (!state.isExpanded) return;
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.unwrapInline('link')
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const href = window.prompt('Enter the URL of the link:', 'http://www.'); // eslint-disable-line
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.wrapInline({
|
|
||||||
type: 'link',
|
|
||||||
data: { href },
|
|
||||||
})
|
|
||||||
.collapseToEnd()
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({ state });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlockTypeClick = (type) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertBlock({
|
|
||||||
type,
|
|
||||||
isVoid: true,
|
|
||||||
})
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state }, this.focusAndAddParagraph);
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePluginClick = (type, data) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertInline({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
isVoid: true,
|
|
||||||
})
|
|
||||||
.collapseToEnd()
|
|
||||||
.insertBlock(DEFAULT_NODE)
|
|
||||||
.focus()
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleImageClick = (mediaProxy) => {
|
|
||||||
let { state } = this.state;
|
|
||||||
this.props.onAddMedia(mediaProxy);
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertBlock(mediaproxyBlock(mediaProxy))
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state });
|
|
||||||
};
|
|
||||||
|
|
||||||
focusAndAddParagraph = () => {
|
|
||||||
const { state } = this.state;
|
|
||||||
const blocks = state.document.getBlocks();
|
|
||||||
const last = blocks.last();
|
|
||||||
const normalized = state
|
|
||||||
.transform()
|
|
||||||
.focus()
|
|
||||||
.collapseToEndOf(last)
|
|
||||||
.splitBlock()
|
|
||||||
.setBlock(DEFAULT_NODE)
|
|
||||||
.apply({
|
|
||||||
snapshot: false,
|
|
||||||
});
|
|
||||||
this.setState({ state: normalized });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (evt) => {
|
|
||||||
if (evt.shiftKey && evt.key === 'Enter') {
|
|
||||||
this.blockEdit = true;
|
|
||||||
let { state } = this.state;
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertText('\n')
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderBlockTypesMenu = () => {
|
handleRef = (ref) => {
|
||||||
const currentBlock = this.state.state.blocks.get(0);
|
this.ref = ref;
|
||||||
const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule');
|
};
|
||||||
|
|
||||||
return (
|
handleHeader = level => (
|
||||||
<BlockTypesMenu
|
() => {
|
||||||
isOpen={isOpen}
|
const { schema } = this.state;
|
||||||
plugins={getPlugins()}
|
const state = this.view.state;
|
||||||
onClickBlock={this.handleBlockTypeClick}
|
const { $from, to, node } = state.selection;
|
||||||
onClickPlugin={this.handlePluginClick}
|
let nodeType = schema.nodes.heading;
|
||||||
onClickImage={this.handleImageClick}
|
let attrs = { level };
|
||||||
/>
|
let inHeader = node && node.hasMarkup(nodeType, attrs);
|
||||||
|
if (!inHeader) {
|
||||||
|
inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs);
|
||||||
|
}
|
||||||
|
if (inHeader) {
|
||||||
|
nodeType = schema.nodes.paragraph;
|
||||||
|
attrs = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = setBlockType(nodeType, { level });
|
||||||
|
command(state, this.handleAction);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
handleBold = () => {
|
||||||
|
const command = toggleMark(this.state.schema.marks.strong);
|
||||||
|
command(this.view.state, this.handleAction);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderStylesMenu() {
|
handleItalic = () => {
|
||||||
const { state } = this.state;
|
const command = toggleMark(this.state.schema.marks.em);
|
||||||
const isOpen = !(state.isBlurred || state.isCollapsed);
|
command(this.view.state, this.handleAction);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
handleLink = () => {
|
||||||
<StylesMenu
|
let url = null;
|
||||||
isOpen={isOpen}
|
if (!markActive(this.view.state, this.state.schema.marks.link)) {
|
||||||
marks={this.state.state.marks}
|
url = prompt('Link URL:');
|
||||||
blocks={this.state.state.blocks}
|
|
||||||
inlines={this.state.state.inlines}
|
|
||||||
onClickMark={this.handleMarkStyleClick}
|
|
||||||
onClickInline={this.handleInlineClick}
|
|
||||||
onClickBlock={this.handleBlockStyleClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null });
|
||||||
|
command(this.view.state, this.handleAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlock = (plugin, data) => {
|
||||||
|
const { schema } = this.state;
|
||||||
|
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
||||||
|
this.view.props.onAction(this.view.state.tr.replaceSelection(nodeType.create(data.toJS())).action());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggle = () => {
|
||||||
|
this.props.onMode('raw');
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
const { onAddMedia, onRemoveMedia, getMedia } = this.props;
|
||||||
<div>
|
const { plugins, showToolbar, showBlockMenu, selectionPosition } = this.state;
|
||||||
{this.renderStylesMenu()}
|
|
||||||
{this.renderBlockTypesMenu()}
|
return (<div className={styles.editor}>
|
||||||
<Editor
|
<Toolbar
|
||||||
placeholder={'Enter some rich text...'}
|
isOpen={showToolbar}
|
||||||
state={this.state.state}
|
selectionPosition={selectionPosition}
|
||||||
schema={SCHEMA}
|
onH1={this.handleHeader(1)}
|
||||||
plugins={this.plugins}
|
onH2={this.handleHeader(2)}
|
||||||
onChange={this.handleChange}
|
onBold={this.handleBold}
|
||||||
onKeyDown={this.handleKeyDown}
|
onItalic={this.handleItalic}
|
||||||
onDocumentChange={this.handleDocumentChange}
|
onLink={this.handleLink}
|
||||||
|
onToggleMode={this.handleToggle}
|
||||||
/>
|
/>
|
||||||
</div>
|
<BlockMenu
|
||||||
);
|
isOpen={showBlockMenu}
|
||||||
|
selectionPosition={selectionPosition}
|
||||||
|
plugins={plugins}
|
||||||
|
onBlock={this.handleBlock}
|
||||||
|
onAddMedia={onAddMedia}
|
||||||
|
onRemoveMedia={onRemoveMedia}
|
||||||
|
getMedia={getMedia}
|
||||||
|
/>
|
||||||
|
<div ref={this.handleRef} />
|
||||||
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Editor.propTypes = {
|
||||||
|
onAddMedia: PropTypes.func.isRequired,
|
||||||
|
onRemoveMedia: PropTypes.func.isRequired,
|
||||||
|
getMedia: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onMode: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.node,
|
||||||
|
};
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
const { wrapIn, setBlockType, chainCommands, newlineInCode, toggleMark } = require('prosemirror-commands');
|
||||||
|
const { selectNextCell, selectPreviousCell } = require('prosemirror-schema-table');
|
||||||
|
const { wrapInList, splitListItem, liftListItem, sinkListItem } = require('prosemirror-schema-list');
|
||||||
|
const { undo, redo } = require('prosemirror-history');
|
||||||
|
|
||||||
|
const mac = typeof navigator != 'undefined' ? /Mac/.test(navigator.platform) : false;
|
||||||
|
|
||||||
|
// :: (Schema, ?Object) → Object
|
||||||
|
// Inspect the given schema looking for marks and nodes from the
|
||||||
|
// basic schema, and if found, add key bindings related to them.
|
||||||
|
// This will add:
|
||||||
|
//
|
||||||
|
// * **Mod-b** for toggling [strong](#schema-basic.StrongMark)
|
||||||
|
// * **Mod-i** for toggling [emphasis](#schema-basic.EmMark)
|
||||||
|
// * **Mod-`** for toggling [code font](#schema-basic.CodeMark)
|
||||||
|
// * **Ctrl-Shift-0** for making the current textblock a paragraph
|
||||||
|
// * **Ctrl-Shift-1** to **Ctrl-Shift-Digit6** for making the current
|
||||||
|
// textblock a heading of the corresponding level
|
||||||
|
// * **Ctrl-Shift-Backslash** to make the current textblock a code block
|
||||||
|
// * **Ctrl-Shift-8** to wrap the selection in an ordered list
|
||||||
|
// * **Ctrl-Shift-9** to wrap the selection in a bullet list
|
||||||
|
// * **Ctrl->** to wrap the selection in a block quote
|
||||||
|
// * **Enter** to split a non-empty textblock in a list item while at
|
||||||
|
// the same time splitting the list item
|
||||||
|
// * **Mod-Enter** to insert a hard break
|
||||||
|
// * **Mod-_** to insert a horizontal rule
|
||||||
|
//
|
||||||
|
// You can suppress or map these bindings by passing a `mapKeys`
|
||||||
|
// argument, which maps key names (say `"Mod-B"` to either `false`, to
|
||||||
|
// remove the binding, or a new key name string.
|
||||||
|
function buildKeymap(schema, mapKeys) {
|
||||||
|
let keys = {}, type;
|
||||||
|
function bind(key, cmd) {
|
||||||
|
if (mapKeys) {
|
||||||
|
const mapped = mapKeys[key];
|
||||||
|
if (mapped === false) return;
|
||||||
|
if (mapped) key = mapped;
|
||||||
|
}
|
||||||
|
keys[key] = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind('Mod-z', undo);
|
||||||
|
bind('Mod-y', redo);
|
||||||
|
|
||||||
|
if (type = schema.marks.strong)
|
||||||
|
bind('Mod-b', toggleMark(type));
|
||||||
|
if (type = schema.marks.em)
|
||||||
|
bind('Mod-i', toggleMark(type));
|
||||||
|
if (type = schema.marks.code)
|
||||||
|
bind('Mod-`', toggleMark(type));
|
||||||
|
|
||||||
|
if (type = schema.nodes.bullet_list)
|
||||||
|
bind('Shift-Ctrl-8', wrapInList(type));
|
||||||
|
if (type = schema.nodes.ordered_list)
|
||||||
|
bind('Shift-Ctrl-9', wrapInList(type));
|
||||||
|
if (type = schema.nodes.blockquote)
|
||||||
|
bind('Ctrl->', wrapIn(type));
|
||||||
|
if (type = schema.nodes.hard_break) {
|
||||||
|
let br = type, cmd = chainCommands(newlineInCode, (state, onAction) => {
|
||||||
|
onAction(state.tr.replaceSelection(br.create()).scrollAction());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
bind('Mod-Enter', cmd);
|
||||||
|
bind('Shift-Enter', cmd);
|
||||||
|
if (mac) bind('Ctrl-Enter', cmd);
|
||||||
|
}
|
||||||
|
if (type = schema.nodes.list_item) {
|
||||||
|
bind('Enter', splitListItem(type));
|
||||||
|
bind('Mod-[', liftListItem(type));
|
||||||
|
bind('Mod-]', sinkListItem(type));
|
||||||
|
}
|
||||||
|
if (type = schema.nodes.paragraph)
|
||||||
|
bind('Shift-Ctrl-0', setBlockType(type));
|
||||||
|
if (type = schema.nodes.code_block)
|
||||||
|
bind('Shift-Ctrl-\\', setBlockType(type));
|
||||||
|
if (type = schema.nodes.heading)
|
||||||
|
for (let i = 1; i <= 6; i++) bind(`Shift-Ctrl-${ i }`, setBlockType(type, { level: i }));
|
||||||
|
if (type = schema.nodes.horizontal_rule) {
|
||||||
|
const hr = type;
|
||||||
|
bind('Mod-_', (state, onAction) => {
|
||||||
|
onAction(state.tr.replaceSelection(hr.create()).scrollAction());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.nodes.table_row) {
|
||||||
|
bind('Tab', selectNextCell);
|
||||||
|
bind('Shift-Tab', selectPreviousCell);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
exports.buildKeymap = buildKeymap;
|
@ -0,0 +1,254 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/*
|
||||||
|
Based closely on
|
||||||
|
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
|
||||||
|
|
||||||
|
Adds a bit of logic allowing editor plugins to hook into the parsing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const markdownit = require("markdown-it")
|
||||||
|
const {Mark} = require("prosemirror-model")
|
||||||
|
|
||||||
|
function maybeMerge(a, b) {
|
||||||
|
if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
|
||||||
|
return a.copy(a.text + b.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluginHandler(schema, plugins) {
|
||||||
|
return (type, attrs, content) => {
|
||||||
|
if (type.name === 'paragraph' && content.length === 1 && content[0].type.name === 'text') {
|
||||||
|
const text = content[0].text;
|
||||||
|
const plugin = plugins.find(plugin => plugin.get('pattern').test(text));
|
||||||
|
if (plugin) {
|
||||||
|
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
||||||
|
const data = plugin.get('fromBlock').call(plugin, text.match(plugin.get('pattern')));
|
||||||
|
return nodeType.create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object used to track the context of a running parse.
|
||||||
|
class MarkdownParseState {
|
||||||
|
constructor(schema, plugins, tokenHandlers) {
|
||||||
|
this.schema = schema
|
||||||
|
this.stack = [{type: schema.nodes.doc, content: []}]
|
||||||
|
this.marks = Mark.none
|
||||||
|
this.tokenHandlers = tokenHandlers
|
||||||
|
this.pluginHandler = pluginHandler(schema, plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
top() {
|
||||||
|
return this.stack[this.stack.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
push(elt) {
|
||||||
|
if (this.stack.length) this.top().content.push(elt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// : (string)
|
||||||
|
// Adds the given text to the current position in the document,
|
||||||
|
// using the current marks as styling.
|
||||||
|
addText(text) {
|
||||||
|
if (!text) return
|
||||||
|
let nodes = this.top().content, last = nodes[nodes.length - 1]
|
||||||
|
let node = this.schema.text(text, this.marks), merged
|
||||||
|
if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged
|
||||||
|
else nodes.push(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// : (Mark)
|
||||||
|
// Adds the given mark to the set of active marks.
|
||||||
|
openMark(mark) {
|
||||||
|
this.marks = mark.addToSet(this.marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// : (Mark)
|
||||||
|
// Removes the given mark from the set of active marks.
|
||||||
|
closeMark(mark) {
|
||||||
|
this.marks = mark.removeFromSet(this.marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTokens(toks) {
|
||||||
|
for (let i = 0; i < toks.length; i++) {
|
||||||
|
let tok = toks[i]
|
||||||
|
let handler = this.tokenHandlers[tok.type]
|
||||||
|
if (!handler)
|
||||||
|
throw new Error("Token type `" + tok.type + "` not supported by Markdown parser")
|
||||||
|
handler(this, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// : (NodeType, ?Object, ?[Node]) → ?Node
|
||||||
|
// Add a node at the current position.
|
||||||
|
addNode(type, attrs, content) {
|
||||||
|
const node = this.pluginHandler(type, attrs, content) || type.createAndFill(attrs, content, this.marks);
|
||||||
|
if (!node) return null
|
||||||
|
this.push(node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// : (NodeType, ?Object)
|
||||||
|
// Wrap subsequent content in a node of the given type.
|
||||||
|
openNode(type, attrs) {
|
||||||
|
this.stack.push({type: type, attrs: attrs, content: []})
|
||||||
|
}
|
||||||
|
|
||||||
|
// : () → ?Node
|
||||||
|
// Close and return the node that is currently on top of the stack.
|
||||||
|
closeNode() {
|
||||||
|
if (this.marks.length) this.marks = Mark.none
|
||||||
|
let info = this.stack.pop()
|
||||||
|
return this.addNode(info.type, info.attrs, info.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrs(given, token) {
|
||||||
|
return given instanceof Function ? given(token) : given
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code content is represented as a single token with a `content`
|
||||||
|
// property in Markdown-it.
|
||||||
|
function noOpenClose(type) {
|
||||||
|
return type == "code_inline" || type == "code_block" || type == "fence"
|
||||||
|
}
|
||||||
|
|
||||||
|
function withoutTrailingNewline(str) {
|
||||||
|
return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenHandlers(schema, tokens) {
|
||||||
|
let handlers = Object.create(null)
|
||||||
|
for (let type in tokens) {
|
||||||
|
let spec = tokens[type]
|
||||||
|
if (spec.block) {
|
||||||
|
let nodeType =schema.nodeType(spec.block);
|
||||||
|
if (noOpenClose(type)) {
|
||||||
|
handlers[type] = (state, tok) => {
|
||||||
|
state.openNode(nodeType, attrs(spec.attrs, tok))
|
||||||
|
state.addText(withoutTrailingNewline(tok.content))
|
||||||
|
state.closeNode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handlers[type + "_open"] = (state, tok) => state.openNode(nodeType, attrs(spec.attrs, tok))
|
||||||
|
handlers[type + "_close"] = state => state.closeNode()
|
||||||
|
}
|
||||||
|
} else if (spec.node) {
|
||||||
|
let nodeType = schema.nodeType(spec.node)
|
||||||
|
handlers[type] = (state, tok) => state.addNode(nodeType, attrs(spec.attrs, tok))
|
||||||
|
} else if (spec.mark) {
|
||||||
|
let markType = schema.marks[spec.mark]
|
||||||
|
if (noOpenClose(type)) {
|
||||||
|
handlers[type] = (state, tok) => {
|
||||||
|
state.openMark(markType.create(attrs(spec.attrs, tok)))
|
||||||
|
state.addText(withoutTrailingNewline(tok.content))
|
||||||
|
state.closeMark(markType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handlers[type + "_open"] = (state, tok) => state.openMark(markType.create(attrs(spec.attrs, tok)))
|
||||||
|
handlers[type + "_close"] = state => state.closeMark(markType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.text = (state, tok) => state.addText(tok.content)
|
||||||
|
handlers.inline = (state, tok) => state.parseTokens(tok.children)
|
||||||
|
handlers.softbreak = state => state.addText("\n")
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
// ;; A configuration of a Markdown parser. Such a parser uses
|
||||||
|
// [markdown-it](https://github.com/markdown-it/markdown-it) to
|
||||||
|
// tokenize a file, and then runs the custom rules it is given over
|
||||||
|
// the tokens to create a ProseMirror document tree.
|
||||||
|
class MarkdownParser {
|
||||||
|
// :: (Schema, MarkdownIt, Object)
|
||||||
|
// Create a parser with the given configuration. You can configure
|
||||||
|
// the markdown-it parser to parse the dialect you want, and provide
|
||||||
|
// a description of the ProseMirror entities those tokens map to in
|
||||||
|
// the `tokens` object, which maps token names to descriptions of
|
||||||
|
// what to do with them. Such a description is an object, and may
|
||||||
|
// have the following properties:
|
||||||
|
//
|
||||||
|
// **`node`**`: ?string`
|
||||||
|
// : This token maps to a single node, whose type can be looked up
|
||||||
|
// in the schema under the given name. Exactly one of `node`,
|
||||||
|
// `block`, or `mark` must be set.
|
||||||
|
//
|
||||||
|
// **`block`**`: ?string`
|
||||||
|
// : This token comes in `_open` and `_close` variants (which are
|
||||||
|
// appended to the base token name provides a the object
|
||||||
|
// property), and wraps a block of content. The block should be
|
||||||
|
// wrapped in a node of the type named to by the property's
|
||||||
|
// value.
|
||||||
|
//
|
||||||
|
// **`mark`**`: ?string`
|
||||||
|
// : This token also comes in `_open` and `_close` variants, but
|
||||||
|
// should add a mark (named by the value) to its content, rather
|
||||||
|
// than wrapping it in a node.
|
||||||
|
//
|
||||||
|
// **`attrs`**`: ?union<Object, (MarkdownToken) → Object>`
|
||||||
|
// : If the mark or node to be created needs attributes, they can
|
||||||
|
// be either given directly, or as a function that takes a
|
||||||
|
// [markdown-it
|
||||||
|
// token](https://markdown-it.github.io/markdown-it/#Token) and
|
||||||
|
// returns an attribute object.
|
||||||
|
constructor(schema, plugins, tokenizer, tokens) {
|
||||||
|
// :: Object The value of the `tokens` object used to construct
|
||||||
|
// this parser. Can be useful to copy and modify to base other
|
||||||
|
// parsers on.
|
||||||
|
this.tokens = tokens
|
||||||
|
this.schema = schema
|
||||||
|
this.tokenizer = tokenizer
|
||||||
|
this.plugins = plugins
|
||||||
|
this.tokenHandlers = tokenHandlers(schema, tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// :: (string) → Node
|
||||||
|
// Parse a string as [CommonMark](http://commonmark.org/) markup,
|
||||||
|
// and create a ProseMirror document as prescribed by this parser's
|
||||||
|
// rules.
|
||||||
|
parse(text) {
|
||||||
|
let state = new MarkdownParseState(this.schema, this.plugins, this.tokenHandlers), doc
|
||||||
|
state.parseTokens(this.tokenizer.parse(text, {}))
|
||||||
|
do { doc = state.closeNode() } while (state.stack.length)
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// :: MarkdownParser
|
||||||
|
// A parser parsing unextended [CommonMark](http://commonmark.org/),
|
||||||
|
// without inline HTML, and producing a document in the basic schema.
|
||||||
|
export default function createMarkdownParser(schema, plugins) {
|
||||||
|
const tokens = {
|
||||||
|
blockquote: {block: "blockquote"},
|
||||||
|
paragraph: {block: "paragraph"},
|
||||||
|
list_item: {block: "list_item"},
|
||||||
|
bullet_list: {block: "bullet_list"},
|
||||||
|
ordered_list: {block: "ordered_list", attrs: tok => ({order: +tok.attrGet("order") || 1})},
|
||||||
|
heading: {block: "heading", attrs: tok => ({level: +tok.tag.slice(1)})},
|
||||||
|
code_block: {block: "code_block"},
|
||||||
|
fence: {block: "code_block"},
|
||||||
|
hr: {node: "horizontal_rule"},
|
||||||
|
image: {node: "image", attrs: tok => ({
|
||||||
|
src: tok.attrGet("src"),
|
||||||
|
title: tok.attrGet("title") || null,
|
||||||
|
alt: tok.children[0] && tok.children[0].content || null
|
||||||
|
})},
|
||||||
|
hardbreak: {node: "hard_break"},
|
||||||
|
|
||||||
|
em: {mark: "em"},
|
||||||
|
strong: {mark: "strong"},
|
||||||
|
link: {mark: "link", attrs: tok => ({
|
||||||
|
href: tok.attrGet("href"),
|
||||||
|
title: tok.attrGet("title") || null
|
||||||
|
})},
|
||||||
|
code_inline: {mark: "code"}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new MarkdownParser(schema, plugins, markdownit("commonmark", {html: false}), tokens);
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Block from './Block';
|
|
||||||
import styles from './index.css';
|
|
||||||
|
|
||||||
/* eslint react/prop-types: 0, react/no-multi-comp: 0 */
|
|
||||||
|
|
||||||
// Define the default node type.
|
|
||||||
export const DEFAULT_NODE = 'paragraph';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define a schema.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const SCHEMA = {
|
|
||||||
nodes: {
|
|
||||||
'blockquote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'unordered_list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
|
||||||
'header_one': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'header_two': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'header_three': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'header_four': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'header_five': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'header_six': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'list_item': props => <li {...props.attributes}>{props.children}</li>,
|
|
||||||
'paragraph': props => <Block type='Paragraph' {...props.attributes}>{props.children}</Block>,
|
|
||||||
'hr': props => {
|
|
||||||
const { node, state } = props;
|
|
||||||
const isFocused = state.selection.hasEdgeIn(node);
|
|
||||||
const className = isFocused ? styles.active : null;
|
|
||||||
return (
|
|
||||||
<hr className={className} {...props.attributes} />
|
|
||||||
);
|
|
||||||
},
|
|
||||||
'link': (props) => {
|
|
||||||
const { data } = props.node;
|
|
||||||
const href = data.get('href');
|
|
||||||
return <a {...props.attributes} href={href}>{props.children}</a>;
|
|
||||||
},
|
|
||||||
'image': (props) => {
|
|
||||||
const { node, state } = props;
|
|
||||||
const isFocused = state.selection.hasEdgeIn(node);
|
|
||||||
const className = isFocused ? styles.active : null;
|
|
||||||
const src = node.data.get('src');
|
|
||||||
return (
|
|
||||||
<img {...props.attributes} src={src} className={className} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
marks: {
|
|
||||||
BOLD: {
|
|
||||||
fontWeight: 'bold'
|
|
||||||
},
|
|
||||||
ITALIC: {
|
|
||||||
fontStyle: 'italic'
|
|
||||||
},
|
|
||||||
CODE: {
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
padding: '3px',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,59 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Portal from 'react-portal';
|
|
||||||
import position from 'selection-position';
|
|
||||||
|
|
||||||
export default function withPortalAtCursorPosition(WrappedComponent) {
|
|
||||||
return class extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
menu: null,
|
|
||||||
cursorPosition: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.adjustPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.adjustPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustPosition = () => {
|
|
||||||
const { menu } = this.state;
|
|
||||||
|
|
||||||
if (!menu) return;
|
|
||||||
|
|
||||||
const cursorPosition = position(); // TODO: Results aren't determenistic
|
|
||||||
const centerX = Math.ceil(
|
|
||||||
cursorPosition.left
|
|
||||||
+ cursorPosition.width / 2
|
|
||||||
+ window.scrollX
|
|
||||||
- menu.offsetWidth / 2
|
|
||||||
);
|
|
||||||
const centerY = cursorPosition.top + window.scrollY;
|
|
||||||
menu.style.opacity = 1;
|
|
||||||
menu.style.top = `${ centerY }px`;
|
|
||||||
menu.style.left = `${ centerX }px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the portal opens, cache the menu element.
|
|
||||||
*/
|
|
||||||
handleOpen = (portal) => {
|
|
||||||
this.setState({ menu: portal.firstChild });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isOpen, ...rest } = this.props;
|
|
||||||
return (
|
|
||||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
|
||||||
<WrappedComponent {...rest} />
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
119
yarn.lock
119
yarn.lock
@ -2872,6 +2872,10 @@ extend@^3.0.0, extend@~3.0.0:
|
|||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
|
||||||
|
|
||||||
|
extending-char@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/extending-char/-/extending-char-1.0.1.tgz#4c6c0eee3658a49df1600b32fc73876f418c7c6c"
|
||||||
|
|
||||||
extglob@^0.3.1:
|
extglob@^0.3.1:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
|
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
|
||||||
@ -4733,6 +4737,12 @@ liftoff@^2.2.0:
|
|||||||
rechoir "^0.6.2"
|
rechoir "^0.6.2"
|
||||||
resolve "^1.1.7"
|
resolve "^1.1.7"
|
||||||
|
|
||||||
|
linkify-it@~1.2.2:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"
|
||||||
|
dependencies:
|
||||||
|
uc.micro "^1.0.1"
|
||||||
|
|
||||||
lint-staged@^3.1.0:
|
lint-staged@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.1.0.tgz#4bb3da3b98135b0a076606c5e4f129af034bfe48"
|
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.1.0.tgz#4bb3da3b98135b0a076606c5e4f129af034bfe48"
|
||||||
@ -5271,6 +5281,16 @@ map-obj@^1.0.0, map-obj@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||||
|
|
||||||
|
markdown-it@^6.0.4:
|
||||||
|
version "6.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-6.1.1.tgz#ced037f4473ee9f5153ac414f77dc83c91ba927c"
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
entities "~1.1.1"
|
||||||
|
linkify-it "~1.2.2"
|
||||||
|
mdurl "~1.0.1"
|
||||||
|
uc.micro "^1.0.1"
|
||||||
|
|
||||||
marked-terminal@^1.6.2:
|
marked-terminal@^1.6.2:
|
||||||
version "1.6.2"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.6.2.tgz#44c128d69b5d9776c848314cdf69d4ec96322973"
|
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.6.2.tgz#44c128d69b5d9776c848314cdf69d4ec96322973"
|
||||||
@ -5310,6 +5330,10 @@ math-expression-evaluator@^1.2.14:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash.indexof "^4.0.5"
|
lodash.indexof "^4.0.5"
|
||||||
|
|
||||||
|
mdurl@~1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
@ -6711,6 +6735,89 @@ proper-lockfile@^1.1.2:
|
|||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
retry "^0.10.0"
|
retry "^0.10.0"
|
||||||
|
|
||||||
|
prosemirror-commands@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-0.12.0.tgz#d790fe3dbabb5221e4d87e82834835e0f65881b2"
|
||||||
|
dependencies:
|
||||||
|
extending-char "^1.0.0"
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-history@^0.12.0:
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-0.12.1.tgz#cbcdb536455b6af36bd2ba3ccced5387e5cfbfe1"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
rope-sequence "^1.2.0"
|
||||||
|
|
||||||
|
prosemirror-inputrules@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-0.12.0.tgz#2e07b5cb1bfc7007c2b51ea5394303204b4b34df"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-keymap@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-0.12.0.tgz#b70645b5d3f5ff4843bc6d26a74fa0022b504221"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
w3c-keyname "^1.1.0"
|
||||||
|
|
||||||
|
prosemirror-markdown@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-0.12.0.tgz#7ff8557c159168dcb532833c0b23b5b2866715c8"
|
||||||
|
dependencies:
|
||||||
|
markdown-it "^6.0.4"
|
||||||
|
prosemirror-model "~0.12.0"
|
||||||
|
|
||||||
|
prosemirror-model@^0.12.0, prosemirror-model@~0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-0.12.0.tgz#5430c4056f2d3fe87d36de3f73aa9d9d07b0e8a7"
|
||||||
|
|
||||||
|
prosemirror-schema-basic@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-0.12.0.tgz#9af876f8a915e75ba65847c794eebfc0df9f274e"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-schema-list@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-0.12.0.tgz#d93ba425ed202fc113d7b3388e5d9be1f698c276"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-schema-table@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-schema-table/-/prosemirror-schema-table-0.12.0.tgz#a665dcb66bbd4c0ff2eac492d82991c6c410b5f3"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-state@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-0.12.0.tgz#16e13d57d91840d0c3c340d47694efabeb77e987"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
prosemirror-transform "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-transform@^0.12.0, prosemirror-transform@^0.12.1:
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-0.12.1.tgz#69bca7e55976815e59281fbd8af4518f5ab90844"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
|
||||||
|
prosemirror-view@^0.12.0:
|
||||||
|
version "0.12.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-0.12.2.tgz#4a48bfe2ae3119b8c0c79166d7cd73e82284c99d"
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "^0.12.0"
|
||||||
|
prosemirror-state "^0.12.0"
|
||||||
|
|
||||||
proxy-addr@~1.1.2:
|
proxy-addr@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37"
|
||||||
@ -7391,6 +7498,10 @@ rollup@^0.36.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map-support "^0.4.0"
|
source-map-support "^0.4.0"
|
||||||
|
|
||||||
|
rope-sequence@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.1.tgz#7da14c04fdc06f60bacdb9d26936c56265ffee2e"
|
||||||
|
|
||||||
rsvp@^3.0.13, rsvp@^3.0.18:
|
rsvp@^3.0.13, rsvp@^3.0.18:
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.3.3.tgz#34633caaf8bc66ceff4be3c2e1dffd032538a813"
|
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.3.3.tgz#34633caaf8bc66ceff4be3c2e1dffd032538a813"
|
||||||
@ -8297,6 +8408,10 @@ ua-parser-js@^0.7.10, ua-parser-js@^0.7.9:
|
|||||||
version "0.7.10"
|
version "0.7.10"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f"
|
||||||
|
|
||||||
|
uc.micro@^1.0.1:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
|
||||||
|
|
||||||
uglify-js@^2.6, uglify-js@^2.6.1:
|
uglify-js@^2.6, uglify-js@^2.6.1:
|
||||||
version "2.7.3"
|
version "2.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.3.tgz#39b3a7329b89f5ec507e344c6e22568698ef4868"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.3.tgz#39b3a7329b89f5ec507e344c6e22568698ef4868"
|
||||||
@ -8468,6 +8583,10 @@ vm-browserify@0.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
indexof "0.0.1"
|
indexof "0.0.1"
|
||||||
|
|
||||||
|
w3c-keyname@^1.1.0:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.1.tgz#0bb8566fbba0e414c2b798b696a71e1726967661"
|
||||||
|
|
||||||
walkdir@0.0.11:
|
walkdir@0.0.11:
|
||||||
version "0.0.11"
|
version "0.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532"
|
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user