refactor: moved styles menu to external component
This commit is contained in:
parent
62d6ece758
commit
36a344f34a
@ -97,7 +97,6 @@ rules:
|
|||||||
react/jsx-no-duplicate-props: 1
|
react/jsx-no-duplicate-props: 1
|
||||||
react/jsx-no-undef: 1
|
react/jsx-no-undef: 1
|
||||||
react/jsx-pascal-case: 1
|
react/jsx-pascal-case: 1
|
||||||
react/jsx-sort-prop-types: 1
|
|
||||||
react/jsx-uses-react: 1
|
react/jsx-uses-react: 1
|
||||||
react/jsx-uses-vars: 1
|
react/jsx-uses-vars: 1
|
||||||
react/no-danger: 1
|
react/no-danger: 1
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Editor, Plain } from 'slate';
|
import { Editor, Plain } from 'slate';
|
||||||
import Portal from 'react-portal';
|
|
||||||
import position from 'selection-position';
|
import position from 'selection-position';
|
||||||
import Markdown from 'slate-markdown-serializer';
|
import Markdown from 'slate-markdown-serializer';
|
||||||
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
||||||
|
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
||||||
import AddBlock from './MarkdownControlElements/AddBlock';
|
import AddBlock from './MarkdownControlElements/AddBlock';
|
||||||
import { Icon } from '../UI';
|
|
||||||
import styles from './MarkdownControl.css';
|
|
||||||
|
|
||||||
const markdown = new Markdown();
|
const markdown = new Markdown();
|
||||||
|
|
||||||
@ -17,6 +15,13 @@ class MarkdownControl extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.blockEdit = false;
|
this.blockEdit = false;
|
||||||
|
this.stylesMenuPosition = {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize(''),
|
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize(''),
|
||||||
addBlockButton:{
|
addBlockButton:{
|
||||||
@ -24,46 +29,17 @@ class MarkdownControl extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hasMark = this.hasMark.bind(this);
|
|
||||||
this.hasBlock = this.hasBlock.bind(this);
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleDocumentChange = this.handleDocumentChange.bind(this);
|
this.handleDocumentChange = this.handleDocumentChange.bind(this);
|
||||||
this.maybeShowBlockAddButton = this.maybeShowBlockAddButton.bind(this);
|
this.maybeShowBlockAddButton = this.maybeShowBlockAddButton.bind(this);
|
||||||
this.onClickMark = this.onClickMark.bind(this);
|
this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this);
|
||||||
this.onClickBlock = this.onClickBlock.bind(this);
|
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
this.renderMenu = this.renderMenu.bind(this);
|
|
||||||
this.renderAddBlock = this.renderAddBlock.bind(this);
|
this.renderAddBlock = this.renderAddBlock.bind(this);
|
||||||
this.renderMarkButton = this.renderMarkButton.bind(this);
|
|
||||||
this.renderBlockButton = this.renderBlockButton.bind(this);
|
|
||||||
this.renderNode = this.renderNode.bind(this);
|
this.renderNode = this.renderNode.bind(this);
|
||||||
this.renderMark = this.renderMark.bind(this);
|
this.renderMark = this.renderMark.bind(this);
|
||||||
this.handleOpen = this.handleOpen.bind(this);
|
|
||||||
this.updateMenu = this.updateMenu.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On update, update the menu.
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.updateMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.updateMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to set toolbar buttons to active state
|
|
||||||
*/
|
|
||||||
hasMark(type) {
|
|
||||||
const { state } = this.state;
|
|
||||||
return state.marks.some(mark => mark.type == type);
|
|
||||||
}
|
|
||||||
hasBlock(type) {
|
|
||||||
const { state } = this.state;
|
|
||||||
return state.blocks.some(node => node.type == type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slate keeps track of selections, scroll position etc.
|
* Slate keeps track of selections, scroll position etc.
|
||||||
@ -103,7 +79,7 @@ class MarkdownControl extends React.Component {
|
|||||||
/**
|
/**
|
||||||
* Toggle marks / blocks when button is clicked
|
* Toggle marks / blocks when button is clicked
|
||||||
*/
|
*/
|
||||||
onClickMark(e, type) {
|
handleMarkStyleClick(type) {
|
||||||
let { state } = this.state;
|
let { state } = this.state;
|
||||||
|
|
||||||
state = state
|
state = state
|
||||||
@ -114,29 +90,13 @@ class MarkdownControl extends React.Component {
|
|||||||
this.setState({ state });
|
this.setState({ state });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(evt) {
|
handleBlockStyleClick(type, isActive, isList) {
|
||||||
if (evt.shiftKey && evt.key === 'Enter') {
|
|
||||||
this.blockEdit = true;
|
|
||||||
let { state } = this.state;
|
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertText(' \n')
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickBlock(e, type) {
|
|
||||||
e.preventDefault();
|
|
||||||
let { state } = this.state;
|
let { state } = this.state;
|
||||||
let transform = state.transform();
|
let transform = state.transform();
|
||||||
const { document } = state;
|
const { document } = state;
|
||||||
|
|
||||||
// Handle everything but list buttons.
|
// Handle everything but list buttons.
|
||||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||||
const isActive = this.hasBlock(type);
|
|
||||||
const isList = this.hasBlock('list-item');
|
|
||||||
|
|
||||||
if (isList) {
|
if (isList) {
|
||||||
transform = transform
|
transform = transform
|
||||||
@ -153,7 +113,6 @@ class MarkdownControl extends React.Component {
|
|||||||
|
|
||||||
// Handle the extra wrapping required for list buttons.
|
// Handle the extra wrapping required for list buttons.
|
||||||
else {
|
else {
|
||||||
const isList = this.hasBlock('list-item');
|
|
||||||
const isType = state.blocks.some((block) => {
|
const isType = state.blocks.some((block) => {
|
||||||
return !!document.getClosest(block, parent => parent.type == type);
|
return !!document.getClosest(block, parent => parent.type == type);
|
||||||
});
|
});
|
||||||
@ -177,29 +136,17 @@ class MarkdownControl extends React.Component {
|
|||||||
this.setState({ state });
|
this.setState({ state });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
handleKeyDown(evt) {
|
||||||
* When the portal opens, cache the menu element.
|
if (evt.shiftKey && evt.key === 'Enter') {
|
||||||
*/
|
this.blockEdit = true;
|
||||||
handleOpen(portal) {
|
let { state } = this.state;
|
||||||
this.setState({ menu: portal.firstChild });
|
state = state
|
||||||
}
|
.transform()
|
||||||
|
.insertText(' \n')
|
||||||
|
.apply();
|
||||||
|
|
||||||
renderMenu() {
|
this.setState({ state });
|
||||||
const { state } = this.state;
|
}
|
||||||
const isOpen = state.isExpanded && state.isFocused;
|
|
||||||
return (
|
|
||||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
|
||||||
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
|
||||||
{this.renderMarkButton('bold', 'bold')}
|
|
||||||
{this.renderMarkButton('italic', 'italic')}
|
|
||||||
{this.renderMarkButton('code', 'code')}
|
|
||||||
{this.renderBlockButton('heading1', 'h1')}
|
|
||||||
{this.renderBlockButton('heading2', 'h2')}
|
|
||||||
{this.renderBlockButton('block-quote', 'quote-left')}
|
|
||||||
{this.renderBlockButton('bulleted-list', 'list-bullet')}
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAddBlock() {
|
renderAddBlock() {
|
||||||
@ -208,28 +155,6 @@ class MarkdownControl extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMarkButton(type, icon) {
|
|
||||||
const isActive = this.hasMark(type);
|
|
||||||
const onMouseDown = e => this.onClickMark(e, type);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
|
||||||
<Icon type={icon}/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBlockButton(type, icon) {
|
|
||||||
const isActive = this.hasBlock(type);
|
|
||||||
const onMouseDown = e => this.onClickBlock(e, type);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
|
||||||
<Icon type={icon}/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return renderers for Slate
|
* Return renderers for Slate
|
||||||
*/
|
*/
|
||||||
@ -243,25 +168,37 @@ class MarkdownControl extends React.Component {
|
|||||||
/**
|
/**
|
||||||
* Update the menu's absolute position.
|
* Update the menu's absolute position.
|
||||||
*/
|
*/
|
||||||
updateMenu() {
|
renderStylesMenu() {
|
||||||
const { menu, state } = this.state;
|
const { state } = this.state;
|
||||||
if (!menu) return;
|
const rect = position();
|
||||||
|
|
||||||
if (state.isBlurred || state.isCollapsed) {
|
const isOpen = !(state.isBlurred || state.isCollapsed);
|
||||||
menu.removeAttribute('style');
|
|
||||||
return;
|
if (isOpen) {
|
||||||
|
this.stylesMenuPosition = {
|
||||||
|
top: rect.top + window.scrollY,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = position();
|
return (
|
||||||
menu.style.opacity = 1;
|
<StylesMenu
|
||||||
menu.style.top = `${rect.top + window.scrollY - menu.offsetHeight}px`;
|
isOpen={isOpen}
|
||||||
menu.style.left = `${rect.left + window.scrollX - menu.offsetWidth / 2 + rect.width / 2}px`;
|
position={this.stylesMenuPosition}
|
||||||
|
marks={this.state.state.marks}
|
||||||
|
blocks={this.state.state.blocks}
|
||||||
|
onClickMark={this.handleMarkStyleClick}
|
||||||
|
onClickBlock={this.handleBlockStyleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.renderMenu()}
|
{this.renderStylesMenu()}
|
||||||
{this.renderAddBlock()}
|
{this.renderAddBlock()}
|
||||||
<Editor
|
<Editor
|
||||||
placeholder={'Enter some rich text...'}
|
placeholder={'Enter some rich text...'}
|
||||||
|
@ -9,7 +9,7 @@ const AVAILABLE_TYPES = [
|
|||||||
'Heading4',
|
'Heading4',
|
||||||
'Heading5',
|
'Heading5',
|
||||||
'Heading6',
|
'Heading6',
|
||||||
'ul',
|
'List',
|
||||||
'blockquote'
|
'blockquote'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
127
src/components/Widgets/MarkdownControlElements/StylesMenu.js
Normal file
127
src/components/Widgets/MarkdownControlElements/StylesMenu.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import Portal from 'react-portal';
|
||||||
|
import { Icon } from '../../UI';
|
||||||
|
import styles from './StylesMenu.css';
|
||||||
|
|
||||||
|
export default class StylesMenu extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
menu: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hasMark = this.hasMark.bind(this);
|
||||||
|
this.hasBlock = this.hasBlock.bind(this);
|
||||||
|
this.renderMarkButton = this.renderMarkButton.bind(this);
|
||||||
|
this.renderBlockButton = this.renderBlockButton.bind(this);
|
||||||
|
this.updateMenuPosition = this.updateMenuPosition.bind(this);
|
||||||
|
this.handleMarkClick = this.handleMarkClick.bind(this);
|
||||||
|
this.handleBlockClick = this.handleBlockClick.bind(this);
|
||||||
|
this.handleOpen = this.handleOpen.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On update, update the menu.
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateMenuPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.updateMenuPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenuPosition() {
|
||||||
|
const { menu } = this.state;
|
||||||
|
const { position } = this.props;
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
menu.style.opacity = 1;
|
||||||
|
menu.style.top = `${position.top - menu.offsetHeight}px`;
|
||||||
|
menu.style.left = `${position.left - menu.offsetWidth / 2 + position.width / 2}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to set toolbar buttons to active state
|
||||||
|
*/
|
||||||
|
hasMark(type) {
|
||||||
|
const { marks } = this.props;
|
||||||
|
return marks.some(mark => mark.type == type);
|
||||||
|
}
|
||||||
|
hasBlock(type) {
|
||||||
|
console.log(type);
|
||||||
|
const { blocks } = this.props;
|
||||||
|
return blocks.some(node => node.type == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const isActive = this.hasBlock(type);
|
||||||
|
const onMouseDown = e => this.handleBlockClick(e, type);
|
||||||
|
return (
|
||||||
|
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
||||||
|
<Icon type={icon}/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the portal opens, cache the menu element.
|
||||||
|
*/
|
||||||
|
handleOpen(portal) {
|
||||||
|
this.setState({ menu: portal.firstChild });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isOpen } = this.props;
|
||||||
|
return (
|
||||||
|
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
||||||
|
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
||||||
|
{this.renderMarkButton('bold', 'bold')}
|
||||||
|
{this.renderMarkButton('italic', 'italic')}
|
||||||
|
{this.renderMarkButton('code', 'code')}
|
||||||
|
{this.renderBlockButton('heading1', 'h1')}
|
||||||
|
{this.renderBlockButton('heading2', 'h2')}
|
||||||
|
{this.renderBlockButton('block-quote', 'quote-left')}
|
||||||
|
{this.renderBlockButton('bulleted-list', 'list-bullet')}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
StylesMenu.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
position: PropTypes.shape({
|
||||||
|
top: PropTypes.number.isRequired,
|
||||||
|
left: PropTypes.number.isRequired
|
||||||
|
}),
|
||||||
|
marks: PropTypes.object.isRequired,
|
||||||
|
blocks: PropTypes.object.isRequired,
|
||||||
|
onClickBlock: PropTypes.func.isRequired,
|
||||||
|
onClickMark: PropTypes.func.isRequired
|
||||||
|
};
|
@ -10,7 +10,7 @@ export const DEFAULT_NODE = 'paragraph';
|
|||||||
// Local node renderers.
|
// Local node renderers.
|
||||||
export const NODES = {
|
export const NODES = {
|
||||||
'block-quote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
'block-quote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
||||||
'bulleted-list': props => <Block type='Unordered List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
'bulleted-list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
||||||
'heading1': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
'heading1': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading2': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'heading2': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading3': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'heading3': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user