Use HoC withPortalAtCursorPosition for StylesMenu and BlockTypesMenu to DRY

This commit is contained in:
Andrey Okonetchnikov 2016-09-29 18:51:39 +02:00
parent cfc8be3f36
commit e454144d31
3 changed files with 97 additions and 112 deletions

View File

@ -1,21 +1,18 @@
import React, { Component, PropTypes } from 'react';
import Portal from 'react-portal';
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
import { Icon } from '../../../UI';
import MediaProxy from '../../../../valueObjects/MediaProxy';
import styles from './BlockTypesMenu.css';
export default class BlockTypesMenu extends Component {
class BlockTypesMenu extends Component {
constructor(props) {
super(props);
this.state = {
expanded: false,
menu: null
expanded: false
};
this.updateMenuPosition = this.updateMenuPosition.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
this.handlePluginClick = this.handlePluginClick.bind(this);
this.handleFileUploadClick = this.handleFileUploadClick.bind(this);
@ -24,34 +21,12 @@ export default class BlockTypesMenu extends Component {
this.renderPluginButton = this.renderPluginButton.bind(this);
}
/**
* On update, update the menu.
*/
componentDidMount() {
this.updateMenuPosition();
}
componentWillUpdate() {
if (this.state.expanded) {
this.setState({ expanded: false });
}
}
componentDidUpdate() {
this.updateMenuPosition();
}
updateMenuPosition() {
const { menu } = this.state;
const { position } = this.props;
if (!menu) return;
menu.style.opacity = 1;
menu.style.top = `${position.top}px`;
menu.style.left = `${position.left - menu.offsetWidth * 2}px`;
}
toggleMenu() {
this.setState({ expanded: !this.state.expanded });
}
@ -63,7 +38,7 @@ export default class BlockTypesMenu extends Component {
handlePluginClick(e, plugin) {
const data = {};
plugin.fields.forEach(field => {
data[field.name] = window.prompt(field.label);
data[field.name] = window.prompt(field.label); // eslint-disable-line
});
this.props.onClickPlugin(plugin.id, data);
}
@ -97,14 +72,14 @@ export default class BlockTypesMenu extends Component {
renderBlockTypeButton(type, icon) {
const onClick = e => this.handleBlockTypeClick(e, type);
return (
<Icon key={type} type={icon} onClick={onClick} className={styles.icon} />
<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} />
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon}/>
);
}
@ -115,13 +90,15 @@ export default class BlockTypesMenu extends Component {
<div className={styles.menu}>
{this.renderBlockTypeButton('hr', 'dot-3')}
{plugins.map(plugin => this.renderPluginButton(plugin))}
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
<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}
ref={el => {
this._fileInput = el;
}}
/>
</div>
);
@ -130,34 +107,21 @@ export default class BlockTypesMenu extends Component {
}
}
/**
* 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.root}>
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu} />
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu}/>
{this.renderMenu()}
</div>
</Portal>
);
}
}
BlockTypesMenu.propTypes = {
isOpen: PropTypes.bool.isRequired,
plugins: PropTypes.array.isRequired,
position: PropTypes.shape({
top: PropTypes.number.isRequired,
left: PropTypes.number.isRequired
}),
onClickBlock: PropTypes.func.isRequired,
onClickPlugin: PropTypes.func.isRequired,
onClickImage: PropTypes.func.isRequired
};
export default withPortalAtCursorPosition(BlockTypesMenu);

View File

@ -1,48 +1,21 @@
import React, { Component, PropTypes } from 'react';
import Portal from 'react-portal';
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
import { Icon } from '../../../UI';
import styles from './StylesMenu.css';
export default class StylesMenu extends Component {
class StylesMenu extends Component {
constructor(props) {
super(props);
this.state = {
menu: null
};
constructor() {
super();
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.renderLinkButton = this.renderLinkButton.bind(this);
this.updateMenuPosition = this.updateMenuPosition.bind(this);
this.handleMarkClick = this.handleMarkClick.bind(this);
this.handleInlineClick = this.handleInlineClick.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`;
}
/**
@ -52,10 +25,12 @@ export default class StylesMenu extends Component {
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');
@ -109,17 +84,8 @@ export default class StylesMenu extends Component {
);
}
/**
* 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')}
@ -130,18 +96,12 @@ export default class StylesMenu extends Component {
{this.renderBlockButton('blockquote', 'quote-left')}
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
</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,
inlines: PropTypes.object.isRequired,
@ -149,3 +109,5 @@ StylesMenu.propTypes = {
onClickMark: PropTypes.func.isRequired,
onClickInline: PropTypes.func.isRequired
};
export default withPortalAtCursorPosition(StylesMenu);

View File

@ -0,0 +1,59 @@
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>
);
}
};
}