refactor: moved styles menu to external component

This commit is contained in:
Cássio Zen 2016-08-04 15:49:43 -03:00
parent 62d6ece758
commit 36a344f34a
6 changed files with 213 additions and 111 deletions

View File

@ -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

View File

@ -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...'}

View File

@ -9,7 +9,7 @@ const AVAILABLE_TYPES = [
'Heading4',
'Heading5',
'Heading6',
'ul',
'List',
'blockquote'
];

View File

@ -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;
}

View 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
};

View File

@ -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>,