Use HoC withPortalAtCursorPosition
for StylesMenu and BlockTypesMenu to DRY
This commit is contained in:
parent
cfc8be3f36
commit
e454144d31
@ -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}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={this.handleFileUploadChange}
|
||||
className={styles.input}
|
||||
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} />
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
</Portal>
|
||||
<div className={styles.root}>
|
||||
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu}/>
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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,39 +84,24 @@ 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')}
|
||||
{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>
|
||||
</Portal>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user