From e454144d31d0ba06e6ae234816bd0770dea7d3ab Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 18:51:39 +0200 Subject: [PATCH] Use HoC `withPortalAtCursorPosition` for StylesMenu and BlockTypesMenu to DRY --- .../VisualEditor/BlockTypesMenu.js | 76 +++++-------------- .../VisualEditor/StylesMenu.js | 74 +++++------------- .../withPortalAtCursorPosition.js | 59 ++++++++++++++ 3 files changed, 97 insertions(+), 112 deletions(-) create mode 100644 src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js index 0855d40b..78c6b5d0 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js @@ -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 ( - + ); } renderPluginButton(plugin) { const onClick = e => this.handlePluginClick(e, plugin); return ( - + ); } @@ -115,13 +90,15 @@ export default class BlockTypesMenu extends Component {
{this.renderBlockTypeButton('hr', 'dot-3')} {plugins.map(plugin => this.renderPluginButton(plugin))} - + this._fileInput = el} + type="file" + accept="image/*" + onChange={this.handleFileUploadChange} + className={styles.input} + ref={el => { + this._fileInput = el; + }} />
); @@ -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 ( - -
- - {this.renderMenu()} -
-
+
+ + {this.renderMenu()} +
); } } 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); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js index f2aafc3e..e2c464c7 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js @@ -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 ( - -
- {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')} -
-
+
+ {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')} +
); } } 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); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js new file mode 100644 index 00000000..4aab4ea6 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js @@ -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 ( + + + + ); + } + }; +}