diff --git a/.eslintrc b/.eslintrc index 99f891d9..b2c61e21 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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 diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 15daa406..76e8727f 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -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 ( - -
- {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')} -
-
- ); + 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 ( - - - - ); - } - - renderBlockButton(type, icon) { - const isActive = this.hasBlock(type); - const onMouseDown = e => this.onClickBlock(e, type); - - return ( - - - - ); - } - /** * 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 ( + + ); } render() { return (
- {this.renderMenu()} + {this.renderStylesMenu()} {this.renderAddBlock()} * { + 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; +} diff --git a/src/components/Widgets/MarkdownControlElements/StylesMenu.js b/src/components/Widgets/MarkdownControlElements/StylesMenu.js new file mode 100644 index 00000000..cdce2dd1 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/StylesMenu.js @@ -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 ( + + + + ); + } + + 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 ( + + + + ); + } + + /** + * 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.renderBlockButton('heading1', 'h1')} + {this.renderBlockButton('heading2', 'h2')} + {this.renderBlockButton('block-quote', 'quote-left')} + {this.renderBlockButton('bulleted-list', 'list-bullet')} +
+
+ ); + } + +} + +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 +}; diff --git a/src/components/Widgets/MarkdownControlElements/localRenderers.js b/src/components/Widgets/MarkdownControlElements/localRenderers.js index 7e955067..a931c9a7 100644 --- a/src/components/Widgets/MarkdownControlElements/localRenderers.js +++ b/src/components/Widgets/MarkdownControlElements/localRenderers.js @@ -10,7 +10,7 @@ export const DEFAULT_NODE = 'paragraph'; // Local node renderers. export const NODES = { 'block-quote': (props) => {props.children}, - 'bulleted-list': props =>
    {props.children}
, + 'bulleted-list': props =>
    {props.children}
, 'heading1': props => {props.children}, 'heading2': props => {props.children}, 'heading3': props => {props.children},