Prosemirror working - with some toolbar options
This commit is contained in:
parent
6015692118
commit
d0df70e4d3
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
.tern-project
|
||||
yarn-error.log
|
||||
|
10
package.json
10
package.json
@ -106,6 +106,16 @@
|
||||
"normalize.css": "^4.2.0",
|
||||
"pluralize": "^3.0.0",
|
||||
"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-schema-basic": "^0.12.0",
|
||||
"prosemirror-schema-list": "^0.12.0",
|
||||
"prosemirror-schema-table": "^0.12.0",
|
||||
"prosemirror-state": "^0.12.0",
|
||||
"prosemirror-view": "^0.12.0",
|
||||
"react": "^15.1.0",
|
||||
"react-addons-css-transition-group": "^15.3.1",
|
||||
"react-datetime": "^2.6.0",
|
||||
|
@ -5,7 +5,7 @@ const availableIcons = [
|
||||
// Font Awesome Editor Icons
|
||||
'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',
|
||||
'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',
|
||||
// Entypo
|
||||
'note', 'note-beamed',
|
||||
|
@ -16,42 +16,44 @@ class MarkdownControl extends React.Component {
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { mode: 'visual' };
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.useRawEditor();
|
||||
processEditorPlugins(registry.getEditorComponents());
|
||||
}
|
||||
|
||||
useVisualEditor = () => {
|
||||
this.props.switchVisualMode(true);
|
||||
};
|
||||
|
||||
useRawEditor = () => {
|
||||
this.props.switchVisualMode(false);
|
||||
handleMode = (mode) => {
|
||||
this.setState({ mode });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editor, onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
|
||||
if (editor.get('useVisualMode')) {
|
||||
const { onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
|
||||
const { mode } = this.state;
|
||||
if (mode === 'visual') {
|
||||
return (
|
||||
<div className="cms-editor-visual">
|
||||
{null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>}
|
||||
<VisualEditor
|
||||
onChange={onChange}
|
||||
onAddMedia={onAddMedia}
|
||||
onRemoveMedia={onRemoveMedia}
|
||||
onMode={this.handleMode}
|
||||
getMedia={getMedia}
|
||||
registeredComponents={editor.get('registeredComponents')}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cms-editor-raw">
|
||||
{null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>}
|
||||
<RawEditor
|
||||
onChange={onChange}
|
||||
onAddMedia={onAddMedia}
|
||||
onRemoveMedia={onRemoveMedia}
|
||||
onMode={this.handleMode}
|
||||
getMedia={getMedia}
|
||||
value={value}
|
||||
/>
|
||||
@ -59,7 +61,6 @@ class MarkdownControl extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({ editor: state.editor }),
|
||||
|
@ -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,7 +6,7 @@ import htmlSyntax from 'markup-it/syntaxes/html';
|
||||
import CaretPosition from 'textarea-caret-position';
|
||||
import registry from '../../../../lib/registry';
|
||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
||||
import Toolbar from './Toolbar';
|
||||
import Toolbar from '../Toolbar';
|
||||
import BlockMenu from './BlockMenu';
|
||||
import styles from './index.css';
|
||||
|
||||
@ -238,8 +238,9 @@ export default class RawEditor extends React.Component {
|
||||
const selection = this.getSelection();
|
||||
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
||||
try {
|
||||
const position = this.caretPosition.get(selection.start, selection.end);
|
||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition: position });
|
||||
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
|
||||
console.log('pos: %o', selectionPosition);
|
||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
||||
} catch (e) {
|
||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||
}
|
||||
@ -313,7 +314,11 @@ export default class RawEditor extends React.Component {
|
||||
this.newSelection = newSelection;
|
||||
onChange(beforeSelection + paste + afterSelection);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleToggle = () => {
|
||||
this.props.onMode('visual');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onAddMedia, onRemoveMedia, getMedia } = this.props;
|
||||
@ -327,6 +332,7 @@ export default class RawEditor extends React.Component {
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
<BlockMenu
|
||||
isOpen={showBlockMenu}
|
||||
@ -353,5 +359,6 @@ RawEditor.propTypes = {
|
||||
onRemoveMedia: PropTypes.func.isRequired,
|
||||
getMedia: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
113
src/components/Widgets/MarkdownControlElements/Toolbar.css
Normal file
113
src/components/Widgets/MarkdownControlElements/Toolbar.css
Normal file
@ -0,0 +1,113 @@
|
||||
.editor {
|
||||
position: relative;
|
||||
|
||||
& h1,
|
||||
& h2,
|
||||
& h3 {
|
||||
margin-bottom: 20px;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
color: #7c8382;
|
||||
text-decoration: none;
|
||||
line-height: 1.45;
|
||||
|
||||
&:before {
|
||||
color: #a5afad;
|
||||
content: '# ';
|
||||
}
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
& h2:before {
|
||||
content: '## ';
|
||||
}
|
||||
|
||||
& h3:before {
|
||||
content: '### ';
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
: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 { Icon } from '../../../UI';
|
||||
import { Icon } from '../../UI';
|
||||
import styles from './Toolbar.css';
|
||||
|
||||
function button(label, icon, action) {
|
||||
@ -19,6 +19,7 @@ export default class Toolbar extends Component {
|
||||
onBold: PropTypes.func.isRequired,
|
||||
onItalic: PropTypes.func.isRequired,
|
||||
onLink: PropTypes.func.isRequired,
|
||||
onToggleMode: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -41,7 +42,7 @@ export default class Toolbar extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen, onH1, onH2, onBold, onItalic, onLink } = this.props;
|
||||
const { isOpen, onH1, onH2, onBold, onItalic, onLink, onToggleMode } = this.props;
|
||||
const classNames = [styles.Toolbar];
|
||||
|
||||
if (isOpen) {
|
||||
@ -55,6 +56,7 @@ export default class Toolbar extends Component {
|
||||
{button('Bold', 'bold', onBold)}
|
||||
{button('Italic', 'italic', onItalic)}
|
||||
{button('Link', 'link', onLink)}
|
||||
{button('View Code', 'code', onToggleMode)}
|
||||
</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,95 @@
|
||||
.active {
|
||||
box-shadow: 0 0 0 2px blue;
|
||||
.editor {
|
||||
position: relative;
|
||||
& h1, & h2, & h3 {
|
||||
padding: 0;
|
||||
color: #7c8382;
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.45;
|
||||
&:before {
|
||||
content: "# ";
|
||||
color: #a5afad;
|
||||
}
|
||||
}
|
||||
& h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
& h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
& h3 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
& h2:before {
|
||||
content: "## ";
|
||||
}
|
||||
& h3:before {
|
||||
content: "### ";
|
||||
}
|
||||
& p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:global .plugin {
|
||||
background-color: #ddd;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
:global {
|
||||
& .ProseMirror {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:global .plugin_icon {
|
||||
font-size: 50px;
|
||||
margin: 12px 0;
|
||||
& .ProseMirror-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
:global .plugin_fields {
|
||||
font-size: 11px;
|
||||
& .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 {
|
||||
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;
|
||||
}
|
||||
|
||||
:global .active {
|
||||
box-shadow: 0 0 0 2px blue;
|
||||
& 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,120 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { Editor, Raw } from 'slate';
|
||||
import PluginDropImages from 'slate-drop-or-paste-images';
|
||||
import MarkupIt, { SlateUtils } from 'markup-it';
|
||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
||||
import { emptyParagraphBlock, mediaproxyBlock } from '../constants';
|
||||
import { DEFAULT_NODE, SCHEMA } from './schema';
|
||||
import { getNodes, getSyntaxes, getPlugins } from '../../richText';
|
||||
import StylesMenu from './StylesMenu';
|
||||
import BlockTypesMenu from './BlockTypesMenu';
|
||||
import React, { Component } from 'react';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import history from 'prosemirror-history';
|
||||
import {
|
||||
blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule,
|
||||
inputRules, allInputRules,
|
||||
} from 'prosemirror-inputrules';
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { schema, defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||
import { buildKeymap } from './keymap';
|
||||
import Toolbar from '../Toolbar';
|
||||
import styles from './index.css';
|
||||
|
||||
/**
|
||||
* Slate Render Configuration
|
||||
*/
|
||||
export default class VisualEditor extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onAddMedia: PropTypes.func.isRequired,
|
||||
getMedia: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
function buildInputRules(schema) {
|
||||
let result = [], type;
|
||||
if (type = schema.nodes.blockquote) result.push(blockQuoteRule(type));
|
||||
if (type = schema.nodes.ordered_list) result.push(orderedListRule(type));
|
||||
if (type = schema.nodes.bullet_list) result.push(bulletListRule(type));
|
||||
if (type = schema.nodes.code_block) result.push(codeBlockRule(type));
|
||||
if (type = schema.nodes.heading) result.push(headingRule(type, 6));
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Editor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const MarkdownSyntax = getSyntaxes(this.getMedia).markdown;
|
||||
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 }),
|
||||
};
|
||||
|
||||
this.plugins = [
|
||||
PluginDropImages({
|
||||
applyTransform: (transform, file) => {
|
||||
const mediaProxy = new MediaProxy(file.name, file);
|
||||
props.onAddMedia(mediaProxy);
|
||||
return transform
|
||||
.insertBlock(mediaproxyBlock(mediaProxy));
|
||||
},
|
||||
componentDidMount() {
|
||||
this.view = new EditorView(this.ref, {
|
||||
state: EditorState.create({
|
||||
doc: defaultMarkdownParser.parse(this.props.value || ''),
|
||||
schema,
|
||||
plugins: [
|
||||
inputRules({
|
||||
rules: allInputRules.concat(buildInputRules(schema)),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
getMedia = (src) => {
|
||||
return this.props.getMedia(src);
|
||||
};
|
||||
|
||||
/**
|
||||
* Slate keeps track of selections, scroll position etc.
|
||||
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
|
||||
* 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);
|
||||
keymap(buildKeymap(schema, {
|
||||
'Mod-z': history.undo,
|
||||
'Mod-y': history.redo,
|
||||
})),
|
||||
keymap(baseKeymap),
|
||||
history.history(),
|
||||
],
|
||||
}),
|
||||
onAction: this.handleAction,
|
||||
});
|
||||
}
|
||||
|
||||
if (isList && isType) {
|
||||
transform = transform
|
||||
.setBlock(DEFAULT_NODE)
|
||||
.unwrapBlock('unordered_list');
|
||||
} else if (isList) {
|
||||
transform = transform
|
||||
.unwrapBlock(type == 'unordered_list')
|
||||
.wrapBlock(type);
|
||||
handleAction = (action) => {
|
||||
const newState = this.view.state.applyAction(action);
|
||||
switch (action.type) {
|
||||
case 'selection':
|
||||
this.handleSelection(newState);
|
||||
default:
|
||||
const md = defaultMarkdownSerializer.serialize(newState.doc);
|
||||
this.props.onChange(md);
|
||||
}
|
||||
this.view.updateState(newState);
|
||||
this.view.focus();
|
||||
};
|
||||
|
||||
handleSelection = (state) => {
|
||||
const { selection } = state;
|
||||
if (selection.from === selection.to) {
|
||||
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, selectionPosition });
|
||||
} else {
|
||||
transform = transform
|
||||
.setBlock('list_item')
|
||||
.wrapBlock(type);
|
||||
}
|
||||
}
|
||||
|
||||
state = transform.apply();
|
||||
this.setState({ state });
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
this.setState({ showToolbar: true });
|
||||
}
|
||||
};
|
||||
|
||||
renderBlockTypesMenu = () => {
|
||||
const currentBlock = this.state.state.blocks.get(0);
|
||||
const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule');
|
||||
handleRef = (ref) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockTypesMenu
|
||||
isOpen={isOpen}
|
||||
plugins={getPlugins()}
|
||||
onClickBlock={this.handleBlockTypeClick}
|
||||
onClickPlugin={this.handlePluginClick}
|
||||
onClickImage={this.handleImageClick}
|
||||
/>
|
||||
handleHeader = level => (
|
||||
() => {
|
||||
const command = setBlockType(schema.nodes.heading, { level });
|
||||
command(this.view.state, this.handleAction);
|
||||
}
|
||||
);
|
||||
|
||||
handleBold = () => {
|
||||
const command = toggleMark(schema.marks.strong);
|
||||
command(this.view.state, this.handleAction);
|
||||
};
|
||||
|
||||
renderStylesMenu() {
|
||||
const { state } = this.state;
|
||||
const isOpen = !(state.isBlurred || state.isCollapsed);
|
||||
handleItalic = () => {
|
||||
const command = toggleMark(schema.marks.em);
|
||||
command(this.view.state, this.handleAction);
|
||||
};
|
||||
|
||||
return (
|
||||
<StylesMenu
|
||||
isOpen={isOpen}
|
||||
marks={this.state.state.marks}
|
||||
blocks={this.state.state.blocks}
|
||||
inlines={this.state.state.inlines}
|
||||
onClickMark={this.handleMarkStyleClick}
|
||||
onClickInline={this.handleInlineClick}
|
||||
onClickBlock={this.handleBlockStyleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
handleToggle = () => {
|
||||
this.props.onMode('raw');
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderStylesMenu()}
|
||||
{this.renderBlockTypesMenu()}
|
||||
<Editor
|
||||
placeholder={'Enter some rich text...'}
|
||||
state={this.state.state}
|
||||
schema={SCHEMA}
|
||||
plugins={this.plugins}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onDocumentChange={this.handleDocumentChange}
|
||||
const { showToolbar, selectionPosition } = this.state;
|
||||
|
||||
return (<div className={styles.editor}>
|
||||
<Toolbar
|
||||
isOpen={showToolbar}
|
||||
selectionPosition={selectionPosition}
|
||||
onH1={this.handleHeader(1)}
|
||||
onH2={this.handleHeader(2)}
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<div ref={this.handleRef} />
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
let 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) {
|
||||
let 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user