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-undef: 1
|
||||
react/jsx-pascal-case: 1
|
||||
react/jsx-sort-prop-types: 1
|
||||
react/jsx-uses-react: 1
|
||||
react/jsx-uses-vars: 1
|
||||
react/no-danger: 1
|
||||
|
@ -1,12 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import Portal from 'react-portal';
|
||||
import position from 'selection-position';
|
||||
import Markdown from 'slate-markdown-serializer';
|
||||
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
||||
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
||||
import AddBlock from './MarkdownControlElements/AddBlock';
|
||||
import { Icon } from '../UI';
|
||||
import styles from './MarkdownControl.css';
|
||||
|
||||
const markdown = new Markdown();
|
||||
|
||||
@ -17,6 +15,13 @@ class MarkdownControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockEdit = false;
|
||||
this.stylesMenuPosition = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
|
||||
this.state = {
|
||||
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize(''),
|
||||
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.handleDocumentChange = this.handleDocumentChange.bind(this);
|
||||
this.maybeShowBlockAddButton = this.maybeShowBlockAddButton.bind(this);
|
||||
this.onClickMark = this.onClickMark.bind(this);
|
||||
this.onClickBlock = this.onClickBlock.bind(this);
|
||||
this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this);
|
||||
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.renderMenu = this.renderMenu.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.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.
|
||||
@ -103,7 +79,7 @@ class MarkdownControl extends React.Component {
|
||||
/**
|
||||
* Toggle marks / blocks when button is clicked
|
||||
*/
|
||||
onClickMark(e, type) {
|
||||
handleMarkStyleClick(type) {
|
||||
let { state } = this.state;
|
||||
|
||||
state = state
|
||||
@ -114,29 +90,13 @@ class MarkdownControl extends React.Component {
|
||||
this.setState({ state });
|
||||
}
|
||||
|
||||
handleKeyDown(evt) {
|
||||
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();
|
||||
handleBlockStyleClick(type, isActive, isList) {
|
||||
let { state } = this.state;
|
||||
let transform = state.transform();
|
||||
const { document } = state;
|
||||
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||
const isActive = this.hasBlock(type);
|
||||
const isList = this.hasBlock('list-item');
|
||||
|
||||
if (isList) {
|
||||
transform = transform
|
||||
@ -153,7 +113,6 @@ class MarkdownControl extends React.Component {
|
||||
|
||||
// Handle the extra wrapping required for list buttons.
|
||||
else {
|
||||
const isList = this.hasBlock('list-item');
|
||||
const isType = state.blocks.some((block) => {
|
||||
return !!document.getClosest(block, parent => parent.type == type);
|
||||
});
|
||||
@ -177,29 +136,17 @@ class MarkdownControl extends React.Component {
|
||||
this.setState({ state });
|
||||
}
|
||||
|
||||
/**
|
||||
* When the portal opens, cache the menu element.
|
||||
*/
|
||||
handleOpen(portal) {
|
||||
this.setState({ menu: portal.firstChild });
|
||||
}
|
||||
handleKeyDown(evt) {
|
||||
if (evt.shiftKey && evt.key === 'Enter') {
|
||||
this.blockEdit = true;
|
||||
let { state } = this.state;
|
||||
state = state
|
||||
.transform()
|
||||
.insertText(' \n')
|
||||
.apply();
|
||||
|
||||
renderMenu() {
|
||||
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>
|
||||
);
|
||||
this.setState({ state });
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -243,25 +168,37 @@ class MarkdownControl extends React.Component {
|
||||
/**
|
||||
* Update the menu's absolute position.
|
||||
*/
|
||||
updateMenu() {
|
||||
const { menu, state } = this.state;
|
||||
if (!menu) return;
|
||||
renderStylesMenu() {
|
||||
const { state } = this.state;
|
||||
const rect = position();
|
||||
|
||||
if (state.isBlurred || state.isCollapsed) {
|
||||
menu.removeAttribute('style');
|
||||
return;
|
||||
const isOpen = !(state.isBlurred || state.isCollapsed);
|
||||
|
||||
if (isOpen) {
|
||||
this.stylesMenuPosition = {
|
||||
top: rect.top + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
};
|
||||
}
|
||||
|
||||
const rect = position();
|
||||
menu.style.opacity = 1;
|
||||
menu.style.top = `${rect.top + window.scrollY - menu.offsetHeight}px`;
|
||||
menu.style.left = `${rect.left + window.scrollX - menu.offsetWidth / 2 + rect.width / 2}px`;
|
||||
return (
|
||||
<StylesMenu
|
||||
isOpen={isOpen}
|
||||
position={this.stylesMenuPosition}
|
||||
marks={this.state.state.marks}
|
||||
blocks={this.state.state.blocks}
|
||||
onClickMark={this.handleMarkStyleClick}
|
||||
onClickBlock={this.handleBlockStyleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderMenu()}
|
||||
{this.renderStylesMenu()}
|
||||
{this.renderAddBlock()}
|
||||
<Editor
|
||||
placeholder={'Enter some rich text...'}
|
||||
|
@ -9,7 +9,7 @@ const AVAILABLE_TYPES = [
|
||||
'Heading4',
|
||||
'Heading5',
|
||||
'Heading6',
|
||||
'ul',
|
||||
'List',
|
||||
'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.
|
||||
export const NODES = {
|
||||
'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>,
|
||||
'heading2': 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