Refactoring and improved error handling around Link Toolbar
This commit is contained in:
@@ -43,7 +43,7 @@ export default function Link({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a {...attributes} href={href} target="_blank">
|
||||
<a {...attributes} href={readOnly ? href : undefined} target="_blank">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -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<any>;
|
||||
@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 {
|
||||
<LinkToolbar
|
||||
{...this.props}
|
||||
link={this.link}
|
||||
onBlur={this.handleBlur}
|
||||
onBlur={this.hideLinkToolbar}
|
||||
/>
|
||||
) : (
|
||||
<FormattingToolbar
|
||||
onCreateLink={this.handleFocus}
|
||||
onCreateLink={this.showLinkToolbar}
|
||||
{...this.props}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
<Separator />
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<ToolbarButton onMouseDown={this.handleCreateLink}>
|
||||
<LinkIcon light />
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
|
||||
@@ -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 (
|
||||
<span>
|
||||
<span ref={ref => (this.wrapper = ref)}>
|
||||
<LinkEditor>
|
||||
<Input
|
||||
innerRef={ref => (this.input = ref)}
|
||||
defaultValue={href}
|
||||
placeholder="Search or paste a link…"
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
autoFocus
|
||||
|
||||
Reference in New Issue
Block a user