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 React, { Component, PropTypes } from 'react';
|
||||||
import Portal from 'react-portal';
|
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
|
||||||
import { Icon } from '../../../UI';
|
import { Icon } from '../../../UI';
|
||||||
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
||||||
import styles from './BlockTypesMenu.css';
|
import styles from './BlockTypesMenu.css';
|
||||||
|
|
||||||
export default class BlockTypesMenu extends Component {
|
class BlockTypesMenu extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
expanded: false,
|
expanded: false
|
||||||
menu: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateMenuPosition = this.updateMenuPosition.bind(this);
|
|
||||||
this.toggleMenu = this.toggleMenu.bind(this);
|
this.toggleMenu = this.toggleMenu.bind(this);
|
||||||
this.handleOpen = this.handleOpen.bind(this);
|
|
||||||
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
|
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
|
||||||
this.handlePluginClick = this.handlePluginClick.bind(this);
|
this.handlePluginClick = this.handlePluginClick.bind(this);
|
||||||
this.handleFileUploadClick = this.handleFileUploadClick.bind(this);
|
this.handleFileUploadClick = this.handleFileUploadClick.bind(this);
|
||||||
@ -24,34 +21,12 @@ export default class BlockTypesMenu extends Component {
|
|||||||
this.renderPluginButton = this.renderPluginButton.bind(this);
|
this.renderPluginButton = this.renderPluginButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On update, update the menu.
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.updateMenuPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUpdate() {
|
componentWillUpdate() {
|
||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
this.setState({ expanded: false });
|
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() {
|
toggleMenu() {
|
||||||
this.setState({ expanded: !this.state.expanded });
|
this.setState({ expanded: !this.state.expanded });
|
||||||
}
|
}
|
||||||
@ -63,7 +38,7 @@ export default class BlockTypesMenu extends Component {
|
|||||||
handlePluginClick(e, plugin) {
|
handlePluginClick(e, plugin) {
|
||||||
const data = {};
|
const data = {};
|
||||||
plugin.fields.forEach(field => {
|
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);
|
this.props.onClickPlugin(plugin.id, data);
|
||||||
}
|
}
|
||||||
@ -97,14 +72,14 @@ export default class BlockTypesMenu extends Component {
|
|||||||
renderBlockTypeButton(type, icon) {
|
renderBlockTypeButton(type, icon) {
|
||||||
const onClick = e => this.handleBlockTypeClick(e, type);
|
const onClick = e => this.handleBlockTypeClick(e, type);
|
||||||
return (
|
return (
|
||||||
<Icon key={type} type={icon} onClick={onClick} className={styles.icon} />
|
<Icon key={type} type={icon} onClick={onClick} className={styles.icon}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPluginButton(plugin) {
|
renderPluginButton(plugin) {
|
||||||
const onClick = e => this.handlePluginClick(e, plugin);
|
const onClick = e => this.handlePluginClick(e, plugin);
|
||||||
return (
|
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}>
|
<div className={styles.menu}>
|
||||||
{this.renderBlockTypeButton('hr', 'dot-3')}
|
{this.renderBlockTypeButton('hr', 'dot-3')}
|
||||||
{plugins.map(plugin => this.renderPluginButton(plugin))}
|
{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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={this.handleFileUploadChange}
|
onChange={this.handleFileUploadChange}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
ref={(el) => this._fileInput = el}
|
ref={el => {
|
||||||
|
this._fileInput = el;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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() {
|
render() {
|
||||||
const { isOpen } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
<div className={styles.root}>
|
||||||
<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()}
|
||||||
{this.renderMenu()}
|
</div>
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockTypesMenu.propTypes = {
|
BlockTypesMenu.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
plugins: PropTypes.array.isRequired,
|
plugins: PropTypes.array.isRequired,
|
||||||
position: PropTypes.shape({
|
|
||||||
top: PropTypes.number.isRequired,
|
|
||||||
left: PropTypes.number.isRequired
|
|
||||||
}),
|
|
||||||
onClickBlock: PropTypes.func.isRequired,
|
onClickBlock: PropTypes.func.isRequired,
|
||||||
onClickPlugin: PropTypes.func.isRequired,
|
onClickPlugin: PropTypes.func.isRequired,
|
||||||
onClickImage: PropTypes.func.isRequired
|
onClickImage: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default withPortalAtCursorPosition(BlockTypesMenu);
|
||||||
|
@ -1,48 +1,21 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import Portal from 'react-portal';
|
import withPortalAtCursorPosition from './withPortalAtCursorPosition';
|
||||||
import { Icon } from '../../../UI';
|
import { Icon } from '../../../UI';
|
||||||
import styles from './StylesMenu.css';
|
import styles from './StylesMenu.css';
|
||||||
|
|
||||||
export default class StylesMenu extends Component {
|
class StylesMenu extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor() {
|
||||||
super(props);
|
super();
|
||||||
|
|
||||||
this.state = {
|
|
||||||
menu: null
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hasMark = this.hasMark.bind(this);
|
this.hasMark = this.hasMark.bind(this);
|
||||||
this.hasBlock = this.hasBlock.bind(this);
|
this.hasBlock = this.hasBlock.bind(this);
|
||||||
this.renderMarkButton = this.renderMarkButton.bind(this);
|
this.renderMarkButton = this.renderMarkButton.bind(this);
|
||||||
this.renderBlockButton = this.renderBlockButton.bind(this);
|
this.renderBlockButton = this.renderBlockButton.bind(this);
|
||||||
this.renderLinkButton = this.renderLinkButton.bind(this);
|
this.renderLinkButton = this.renderLinkButton.bind(this);
|
||||||
this.updateMenuPosition = this.updateMenuPosition.bind(this);
|
|
||||||
this.handleMarkClick = this.handleMarkClick.bind(this);
|
this.handleMarkClick = this.handleMarkClick.bind(this);
|
||||||
this.handleInlineClick = this.handleInlineClick.bind(this);
|
this.handleInlineClick = this.handleInlineClick.bind(this);
|
||||||
this.handleBlockClick = this.handleBlockClick.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;
|
const { marks } = this.props;
|
||||||
return marks.some(mark => mark.type == type);
|
return marks.some(mark => mark.type == type);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBlock(type) {
|
hasBlock(type) {
|
||||||
const { blocks } = this.props;
|
const { blocks } = this.props;
|
||||||
return blocks.some(node => node.type == type);
|
return blocks.some(node => node.type == type);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLinks(type) {
|
hasLinks(type) {
|
||||||
const { inlines } = this.props;
|
const { inlines } = this.props;
|
||||||
return inlines.some(inline => inline.type == 'link');
|
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() {
|
render() {
|
||||||
const { isOpen } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
||||||
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
{this.renderMarkButton('BOLD', 'bold')}
|
||||||
{this.renderMarkButton('BOLD', 'bold')}
|
{this.renderMarkButton('ITALIC', 'italic')}
|
||||||
{this.renderMarkButton('ITALIC', 'italic')}
|
{this.renderMarkButton('CODE', 'code')}
|
||||||
{this.renderMarkButton('CODE', 'code')}
|
{this.renderLinkButton()}
|
||||||
{this.renderLinkButton()}
|
{this.renderBlockButton('header_one', 'h1')}
|
||||||
{this.renderBlockButton('header_one', 'h1')}
|
{this.renderBlockButton('header_two', 'h2')}
|
||||||
{this.renderBlockButton('header_two', 'h2')}
|
{this.renderBlockButton('blockquote', 'quote-left')}
|
||||||
{this.renderBlockButton('blockquote', 'quote-left')}
|
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
|
||||||
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
|
</div>
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StylesMenu.propTypes = {
|
StylesMenu.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
position: PropTypes.shape({
|
|
||||||
top: PropTypes.number.isRequired,
|
|
||||||
left: PropTypes.number.isRequired
|
|
||||||
}),
|
|
||||||
marks: PropTypes.object.isRequired,
|
marks: PropTypes.object.isRequired,
|
||||||
blocks: PropTypes.object.isRequired,
|
blocks: PropTypes.object.isRequired,
|
||||||
inlines: PropTypes.object.isRequired,
|
inlines: PropTypes.object.isRequired,
|
||||||
@ -149,3 +109,5 @@ StylesMenu.propTypes = {
|
|||||||
onClickMark: PropTypes.func.isRequired,
|
onClickMark: PropTypes.func.isRequired,
|
||||||
onClickInline: 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