Files
outline/app/components/Editor/components/Toolbar/Toolbar.js
Tom Moor 15e8e50601 Slate 30
2017-12-02 23:14:27 -08:00

155 lines
3.5 KiB
JavaScript

// @flow
import React, { Component } from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { Portal } from 'react-portal';
import { Editor } from 'slate-react';
import type { value } from 'slate-prop-types';
import styled from 'styled-components';
import _ from 'lodash';
import FormattingToolbar from './components/FormattingToolbar';
import LinkToolbar from './components/LinkToolbar';
@observer
export default class Toolbar extends Component {
@observable active: boolean = false;
@observable focused: boolean = false;
@observable link: ?React$Element<any>;
@observable top: string = '';
@observable left: string = '';
props: {
editor: Editor,
value: value,
};
menu: HTMLElement;
componentDidMount = () => {
this.update();
};
componentDidUpdate = () => {
this.update();
};
handleFocus = () => {
this.focused = true;
};
handleBlur = () => {
this.focused = false;
};
get linkInSelection(): any {
const { value } = this.props;
try {
const selectedLinks = value.startBlock
.getInlinesAtRange(value.selection)
.filter(node => node.type === 'link');
if (selectedLinks.size) {
return selectedLinks.first();
}
} catch (err) {
//
}
}
update = () => {
const { value } = this.props;
const link = this.linkInSelection;
if (value.isBlurred || (value.isCollapsed && !link)) {
if (this.active && !this.focused) {
this.active = false;
this.link = undefined;
this.top = '';
this.left = '';
}
return;
}
// don't display toolbar for document title
const firstNode = value.document.nodes.first();
if (firstNode === value.startBlock) return;
// don't display toolbar for code blocks
if (value.startBlock.type === 'code') return;
this.active = true;
this.focused = !!link;
this.link = link;
const padding = 16;
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.top === 0 && rect.left === 0) {
return;
}
const left =
rect.left + window.scrollX - this.menu.offsetWidth / 2 + rect.width / 2;
this.top = `${Math.round(
rect.top + window.scrollY - this.menu.offsetHeight
)}px`;
this.left = `${Math.round(Math.max(padding, left))}px`;
};
setRef = (ref: HTMLElement) => {
this.menu = ref;
};
render() {
const style = {
top: this.top,
left: this.left,
};
return (
<Portal>
<Menu active={this.active} innerRef={this.setRef} style={style}>
{this.link ? (
<LinkToolbar
{...this.props}
link={this.link}
onBlur={this.handleBlur}
/>
) : (
<FormattingToolbar
onCreateLink={this.handleFocus}
{...this.props}
/>
)}
</Menu>
</Portal>
);
}
}
const Menu = styled.div`
padding: 8px 16px;
position: absolute;
z-index: 2;
top: -10000px;
left: -10000px;
opacity: 0;
background-color: #2f3336;
border-radius: 4px;
transform: scale(0.95);
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275),
transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
line-height: 0;
height: 40px;
min-width: 260px;
${({ active }) =>
active &&
`
transform: translateY(-6px) scale(1);
opacity: 1;
`};
`;