diff --git a/frontend/components/DropdownMenu/DropdownMenu.js b/frontend/components/DropdownMenu/DropdownMenu.js index d97f2dc3..949ca308 100644 --- a/frontend/components/DropdownMenu/DropdownMenu.js +++ b/frontend/components/DropdownMenu/DropdownMenu.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { Component } from 'react'; import invariant from 'invariant'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -10,14 +10,14 @@ import { color } from 'styles/constants'; import { fadeAndScaleIn } from 'styles/animations'; type Props = { - label: React.Element, - onShow?: () => void, + label: React.Element<*>, + onOpen?: () => void, onClose?: () => void, - children?: React.Element, + children?: React.Element<*>, style?: Object, }; -@observer class DropdownMenu extends React.Component { +@observer class DropdownMenu extends Component { props: Props; actionRef: Object; @observable open: boolean = false; @@ -37,7 +37,7 @@ type Props = { this.open = true; this.top = targetRect.bottom - bodyRect.top; this.right = bodyRect.width - targetRect.left - targetRect.width; - if (this.props.onShow) this.props.onShow(); + if (this.props.onOpen) this.props.onOpen(); } }; diff --git a/frontend/components/DropdownMenu/DropdownMenuItem.js b/frontend/components/DropdownMenu/DropdownMenuItem.js index 3ceb8e10..18d824ce 100644 --- a/frontend/components/DropdownMenu/DropdownMenuItem.js +++ b/frontend/components/DropdownMenu/DropdownMenuItem.js @@ -7,7 +7,7 @@ const DropdownMenuItem = ({ onClick, children, }: { - onClick?: () => void, + onClick?: SyntheticEvent => void, children?: React.Element, }) => { return ( @@ -24,11 +24,15 @@ const MenuItem = styled.div` color: ${color.slateDark}; display: flex; - justify-content: space-between; + justify-content: left; align-items: center; cursor: pointer; font-size: 15px; + svg { + margin-right: 8px; + } + a { text-decoration: none; width: 100%; diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 4c41118d..74779c25 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -8,6 +8,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles'; import Flex from 'components/Flex'; import ClickablePadding from './components/ClickablePadding'; import Toolbar from './components/Toolbar'; +import BlockInsert from './components/BlockInsert'; import Placeholder from './components/Placeholder'; import Markdown from './serializer'; import createSchema from './schema'; @@ -173,6 +174,8 @@ type KeyData = { }; render = () => { + const { readOnly, emoji, onSave } = this.props; + return ( -
- +
+ {!readOnly && + } + {!readOnly && + } (this.editor = ref)} placeholder="Start with a title…" bodyPlaceholder="…the rest is your canvas" schema={this.schema} plugins={this.plugins} - emoji={this.props.emoji} + emoji={emoji} state={this.state.state} onKeyDown={this.onKeyDown} onChange={this.onChange} onDocumentChange={this.onDocumentChange} - onSave={this.props.onSave} - readOnly={this.props.readOnly} + onSave={onSave} + readOnly={readOnly} /> diff --git a/frontend/components/Editor/components/BlockInsert.js b/frontend/components/Editor/components/BlockInsert.js new file mode 100644 index 00000000..fdc7fb60 --- /dev/null +++ b/frontend/components/Editor/components/BlockInsert.js @@ -0,0 +1,186 @@ +// @flow +import React, { Component } from 'react'; +import EditList from '../plugins/EditList'; +import getDataTransferFiles from 'utils/getDataTransferFiles'; +import Portal from 'react-portal'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +import styled from 'styled-components'; +import { color } from 'styles/constants'; +import Icon from 'components/Icon'; +import BlockMenu from 'menus/BlockMenu'; +import type { State } from '../types'; + +const { transforms } = EditList; + +type Props = { + state: State, + onChange: Function, + onInsertImage: File => Promise<*>, +}; + +@observer +export default class BlockInsert extends Component { + props: Props; + mouseMoveTimeout: number; + file: HTMLInputElement; + + @observable active: boolean = false; + @observable menuOpen: boolean = false; + @observable top: number; + @observable left: number; + @observable mouseX: number; + + componentDidMount = () => { + this.update(); + window.addEventListener('mousemove', this.handleMouseMove); + }; + + componentWillUpdate = (nextProps: Props) => { + this.update(nextProps); + }; + + componentWillUnmount = () => { + window.removeEventListener('mousemove', this.handleMouseMove); + }; + + setInactive = () => { + if (this.menuOpen) return; + this.active = false; + }; + + handleMouseMove = (ev: SyntheticMouseEvent) => { + const windowWidth = window.innerWidth / 3; + let active = ev.clientX < windowWidth; + + if (active !== this.active) { + this.active = active || this.menuOpen; + } + if (active) { + clearTimeout(this.mouseMoveTimeout); + this.mouseMoveTimeout = setTimeout(this.setInactive, 2000); + } + }; + + handleMenuOpen = () => { + this.menuOpen = true; + }; + + handleMenuClose = () => { + this.menuOpen = false; + }; + + update = (props?: Props) => { + if (!document.activeElement) return; + const { state } = props || this.props; + const boxRect = document.activeElement.getBoundingClientRect(); + const selection = window.getSelection(); + if (!selection.focusNode) return; + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + if (rect.top <= 0 || boxRect.left <= 0) return; + + if (state.startBlock.type === 'heading1') { + this.active = false; + } + + this.top = Math.round(rect.top + window.scrollY); + this.left = Math.round(boxRect.left + window.scrollX - 20); + }; + + onClickBlock = ( + ev: SyntheticEvent, + type: string | Object, + wrapBlock?: string + ) => { + ev.preventDefault(); + let { state } = this.props; + let transform = state.transform(); + const { document } = state; + const parent = document.getParent(state.startBlock.key); + + // lists get some special treatment + if (parent && parent.type === 'list-item') { + transform = transforms.unwrapList( + transforms + .splitListItem(transform.collapseToStart()) + .collapseToEndOfPreviousBlock() + ); + } + + transform = transform.insertBlock(type); + + if (wrapBlock) transform = transform.wrapBlock(wrapBlock); + + state = transform.focus().apply(); + this.props.onChange(state); + this.active = false; + }; + + onPickImage = (ev: SyntheticEvent) => { + // simulate a click on the file upload input element + this.file.click(); + }; + + onChooseImage = async (ev: SyntheticEvent) => { + const files = getDataTransferFiles(ev); + for (const file of files) { + await this.props.onInsertImage(file); + } + }; + + render() { + const style = { top: `${this.top}px`, left: `${this.left}px` }; + const todo = { type: 'list-item', data: { checked: false } }; + + return ( + + + (this.file = ref)} + onChange={this.onChooseImage} + accept="image/*" + /> + } + onPickImage={this.onPickImage} + onInsertList={ev => + this.onClickBlock(ev, 'list-item', 'bulleted-list')} + onInsertTodoList={ev => this.onClickBlock(ev, todo, 'todo-list')} + onInsertBreak={ev => this.onClickBlock(ev, 'horizontal-rule')} + onOpen={this.handleMenuOpen} + onClose={this.handleMenuClose} + /> + + + ); + } +} + +const HiddenInput = styled.input` + position: absolute; + top: -100px; + left: -100px; + visibility: hidden; +`; + +const Trigger = styled.div` + position: absolute; + z-index: 1; + opacity: 0; + background-color: ${color.white}; + border-radius: 4px; + transition: opacity 250ms ease-in-out, transform 250ms ease-in-out; + line-height: 0; + height: 16px; + width: 16px; + transform: scale(.9); + + ${({ active }) => active && ` + transform: scale(1); + opacity: .9; + `} +`; diff --git a/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js b/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js index 2b1938bf..c56ef925 100644 --- a/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon'; import ItalicIcon from 'components/Icon/ItalicIcon'; import LinkIcon from 'components/Icon/LinkIcon'; import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; -import BulletedListIcon from 'components/Icon/BulletedListIcon'; export default class FormattingToolbar extends Component { props: { @@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component { {this.renderMarkButton('deleted', StrikethroughIcon)} {this.renderBlockButton('heading1', Heading1Icon)} {this.renderBlockButton('heading2', Heading2Icon)} - {this.renderBlockButton('bulleted-list', BulletedListIcon)} {this.renderMarkButton('code', CodeIcon)} diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index c487df2a..30688ea1 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -1,11 +1,11 @@ // @flow import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; -import EditList from 'slate-edit-list'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; import EditCode from 'slate-edit-code'; import Prism from 'slate-prism'; +import EditList from './plugins/EditList'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; import insertImage from './insertImage'; @@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { ); }, }), - EditList({ - types: ['ordered-list', 'bulleted-list', 'todo-list'], - typeItem: 'list-item', - }), + EditList, EditCode({ onlyIn: onlyInCode, containerType: 'code', diff --git a/frontend/components/Editor/plugins/EditList.js b/frontend/components/Editor/plugins/EditList.js new file mode 100644 index 00000000..7bef46f4 --- /dev/null +++ b/frontend/components/Editor/plugins/EditList.js @@ -0,0 +1,7 @@ +// @flow +import EditList from 'slate-edit-list'; + +export default EditList({ + types: ['ordered-list', 'bulleted-list', 'todo-list'], + typeItem: 'list-item', +}); diff --git a/frontend/components/Layout/components/SidebarCollections.js b/frontend/components/Layout/components/SidebarCollections.js index 8bd83eb5..a1cf5aa2 100644 --- a/frontend/components/Layout/components/SidebarCollections.js +++ b/frontend/components/Layout/components/SidebarCollections.js @@ -95,7 +95,7 @@ type Props = { (this.menuOpen = true)} + onOpen={() => (this.menuOpen = true)} onClose={() => (this.menuOpen = false)} onImport={this.handleImport} open={this.menuOpen} diff --git a/frontend/menus/BlockMenu.js b/frontend/menus/BlockMenu.js new file mode 100644 index 00000000..5cd3e611 --- /dev/null +++ b/frontend/menus/BlockMenu.js @@ -0,0 +1,49 @@ +// @flow +import React, { Component } from 'react'; +import Icon from 'components/Icon'; +import { observer } from 'mobx-react'; +import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; + +@observer class BlockMenu extends Component { + props: { + label?: React$Element<*>, + onPickImage: SyntheticEvent => void, + onInsertList: SyntheticEvent => void, + onInsertTodoList: SyntheticEvent => void, + onInsertBreak: SyntheticEvent => void, + }; + + render() { + const { + label, + onPickImage, + onInsertList, + onInsertTodoList, + onInsertBreak, + ...rest + } = this.props; + + return ( + + + Add images + + + Start list + + + Start checklist + + + Add break + + + ); + } +} + +export default BlockMenu; diff --git a/frontend/menus/CollectionMenu.js b/frontend/menus/CollectionMenu.js index 6b57bf09..fc29530b 100644 --- a/frontend/menus/CollectionMenu.js +++ b/frontend/menus/CollectionMenu.js @@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; @observer class CollectionMenu extends Component { props: { label?: React$Element, - onShow?: () => void, + onOpen?: () => void, onClose?: () => void, onImport?: () => void, history: Object, @@ -36,13 +36,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; }; render() { - const { collection, label, onShow, onClose, onImport } = this.props; + const { collection, label, onOpen, onClose, onImport } = this.props; const { allowDelete } = collection; return ( } - onShow={onShow} + onOpen={onOpen} onClose={onClose} > {collection &&