diff --git a/app/components/Editor/components/Link.js b/app/components/Editor/components/Link.js index 54302eb60..57a99feb6 100644 --- a/app/components/Editor/components/Link.js +++ b/app/components/Editor/components/Link.js @@ -43,7 +43,7 @@ export default function Link({ ); } else { return ( - + {children} ); diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 9e5e16b2f..5721ed8f9 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -4,17 +4,29 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Portal } from 'react-portal'; import { Editor, findDOMNode } from 'slate-react'; -import { Value } from 'slate'; +import { Node, Value } from 'slate'; import styled from 'styled-components'; import _ from 'lodash'; import FormattingToolbar from './components/FormattingToolbar'; import LinkToolbar from './components/LinkToolbar'; +function getLinkInSelection(value): any { + try { + const selectedLinks = value.document + .getInlinesAtRange(value.selection) + .filter(node => node.type === 'link'); + if (selectedLinks.size) { + return selectedLinks.first(); + } + } catch (err) { + // It's okay. + } +} + @observer export default class Toolbar extends Component { @observable active: boolean = false; - @observable focused: boolean = false; - @observable link: ?React$Element; + @observable link: ?Node; @observable top: string = ''; @observable left: string = ''; @@ -33,35 +45,24 @@ export default class Toolbar extends Component { this.update(); }; - handleFocus = () => { - this.focused = true; + hideLinkToolbar = () => { + this.link = undefined; }; - handleBlur = () => { - this.focused = false; + showLinkToolbar = (ev: SyntheticEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + const link = getLinkInSelection(this.props.value); + this.link = link; }; - get linkInSelection(): any { - const { value } = this.props; - - try { - const selectedLinks = value.document - .getInlinesAtRange(value.selection) - .filter(node => node.type === 'link'); - if (selectedLinks.size) { - return selectedLinks.first(); - } - } catch (err) { - // It's okay. - } - } - update = () => { const { value } = this.props; - const link = this.linkInSelection; + const link = getLinkInSelection(value); if (value.isBlurred || (value.isCollapsed && !link)) { - if (this.active && !this.focused) { + if (this.active && !this.link) { this.active = false; this.link = undefined; this.top = ''; @@ -78,17 +79,20 @@ export default class Toolbar extends Component { if (value.startBlock.type.match(/code/)) return; this.active = true; - this.focused = !!link; - this.link = link; + this.link = this.link || link; const padding = 16; const selection = window.getSelection(); - const range = selection.getRangeAt(0); - const rect = link - ? findDOMNode(link).getBoundingClientRect() - : range.getBoundingClientRect(); + let rect; - if (rect.top === 0 && rect.left === 0) { + if (link) { + rect = findDOMNode(link).getBoundingClientRect(); + } else if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + rect = range.getBoundingClientRect(); + } + + if (!rect || (rect.top === 0 && rect.left === 0)) { return; } @@ -117,11 +121,11 @@ export default class Toolbar extends Component { ) : ( )} diff --git a/app/components/Editor/components/Toolbar/components/DocumentResult.js b/app/components/Editor/components/Toolbar/components/DocumentResult.js index 68aa6f9af..6f0c83e0e 100644 --- a/app/components/Editor/components/Toolbar/components/DocumentResult.js +++ b/app/components/Editor/components/Toolbar/components/DocumentResult.js @@ -25,10 +25,12 @@ function DocumentResult({ document, ...rest }: Props) { const ListItem = styled.a` display: flex; align-items: center; - height: 24px; - padding: 4px 8px 4px 0; + height: 28px; + padding: 6px 8px 6px 0; color: ${color.white}; font-size: 15px; + overflow: hidden; + white-space: nowrap; i { visibility: hidden; diff --git a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js index 77fd7839a..dcf21e074 100644 --- a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -14,7 +14,7 @@ import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; class FormattingToolbar extends Component { props: { editor: Editor, - onCreateLink: () => void, + onCreateLink: SyntheticEvent => void, }; /** @@ -48,15 +48,15 @@ class FormattingToolbar extends Component { this.props.editor.change(change => change.setBlock(type)); }; - onCreateLink = (ev: SyntheticEvent) => { + handleCreateLink = (ev: SyntheticEvent) => { ev.preventDefault(); ev.stopPropagation(); const data = { href: '' }; - this.props.editor.change(change => - change.wrapInline({ type: 'link', data }) - ); - this.props.onCreateLink(); + this.props.editor.change(change => { + change.wrapInline({ type: 'link', data }); + this.props.onCreateLink(ev); + }); }; renderMarkButton = (type: string, IconClass: Function) => { @@ -93,7 +93,7 @@ class FormattingToolbar extends Component { {this.renderBlockButton('heading1', Heading1Icon)} {this.renderBlockButton('heading2', Heading2Icon)} - + diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index d3209e8cc..afe84be60 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -1,9 +1,10 @@ // @flow import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; +import { findDOMNode } from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; +import { Node } from 'slate'; import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; @@ -19,12 +20,13 @@ import Flex from 'shared/components/Flex'; @keydown @observer class LinkToolbar extends Component { + wrapper: HTMLSpanElement; input: HTMLElement; firstDocument: HTMLElement; props: { editor: Editor, - link: Object, + link: Node, documents: DocumentsStore, onBlur: () => void, }; @@ -34,10 +36,35 @@ class LinkToolbar extends Component { @observable resultIds: string[] = []; @observable searchTerm: ?string = null; - componentWillMount() { + componentDidMount() { this.isEditing = !!this.props.link.data.get('href'); + setImmediate(() => + window.addEventListener('click', this.handleOutsideMouseClick) + ); } + componentWillUnmount() { + window.removeEventListener('click', this.handleOutsideMouseClick); + } + + handleOutsideMouseClick = (ev: SyntheticMouseEvent) => { + const element = findDOMNode(this.wrapper); + + if ( + !element || + (ev.target instanceof HTMLElement && element.contains(ev.target)) || + (ev.button && ev.button !== 0) + ) { + return; + } + + if (this.input.value) { + this.props.onBlur(); + } else { + this.removeLink(); + } + }; + @action search = async () => { this.isFetching = true; @@ -70,7 +97,7 @@ class LinkToolbar extends Component { case 40: // down ev.preventDefault(); if (this.firstDocument) { - const element = ReactDOM.findDOMNode(this.firstDocument); + const element = findDOMNode(this.firstDocument); if (element instanceof HTMLElement) element.focus(); } break; @@ -90,16 +117,6 @@ class LinkToolbar extends Component { this.resultIds = []; }; - onBlur = () => { - if (!this.resultIds.length) { - if (this.input.value) { - this.props.onBlur(); - } else { - this.removeLink(); - } - } - }; - removeLink = () => { this.save(''); }; @@ -110,13 +127,15 @@ class LinkToolbar extends Component { }; save = (href: string) => { + const { editor, link } = this.props; href = href.trim(); - this.props.editor.change(change => { + editor.change(change => { if (href) { change.setInline({ type: 'link', data: { href } }); - } else { - change.unwrapInline('link'); + } else if (link) { + change.unwrapInlineByKey(link.key); } + change.deselect(); this.props.onBlur(); }); }; @@ -126,17 +145,17 @@ class LinkToolbar extends Component { }; render() { - const href = this.props.link.data.get('href'); + const { link } = this.props; + const href = link && link.data.get('href'); const hasResults = this.resultIds.length > 0; return ( - + (this.wrapper = ref)}> (this.input = ref)} defaultValue={href} placeholder="Search or paste a link…" - onBlur={this.onBlur} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus