// @flow import React, { Component } from 'react'; 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/build'; import ToolbarButton from './ToolbarButton'; import DocumentResult from './DocumentResult'; import DocumentsStore from 'stores/DocumentsStore'; import keydown from 'react-keydown'; import CloseIcon from 'components/Icon/CloseIcon'; import OpenIcon from 'components/Icon/OpenIcon'; import TrashIcon from 'components/Icon/TrashIcon'; import Flex from 'shared/components/Flex'; @keydown @observer class LinkToolbar extends Component { wrapper: HTMLSpanElement; input: HTMLElement; firstDocument: HTMLElement; props: { editor: Editor, link: Node, documents: DocumentsStore, onBlur: () => void, }; @observable isEditing: boolean = false; @observable isFetching: boolean = false; @observable resultIds: string[] = []; @observable searchTerm: ?string = null; 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; } this.close(); }; close = () => { if (this.input.value) { this.props.onBlur(); } else { this.removeLink(); } }; @action search = async () => { this.isFetching = true; if (this.searchTerm) { try { this.resultIds = await this.props.documents.search(this.searchTerm); } catch (err) { console.error(err); } } else { this.resultIds = []; } this.isFetching = false; }; selectDocument = (ev, document) => { ev.preventDefault(); this.save(document.url); }; onKeyDown = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => { switch (ev.keyCode) { case 13: // enter ev.preventDefault(); return this.save(ev.target.value); case 27: // escape return this.close(); case 40: // down ev.preventDefault(); if (this.firstDocument) { const element = findDOMNode(this.firstDocument); if (element instanceof HTMLElement) element.focus(); } break; default: } }; onChange = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => { try { new URL(ev.target.value); } catch (err) { // this is not a valid url, show search suggestions this.searchTerm = ev.target.value; this.search(); return; } this.resultIds = []; }; removeLink = () => { this.save(''); }; openLink = () => { const href = this.props.link.data.get('href'); window.open(href, '_blank'); }; save = (href: string) => { const { editor, link } = this.props; href = href.trim(); editor.change(change => { if (href) { change.setInline({ type: 'link', data: { href } }); } else if (link) { const selContainsLink = !!change.value.startBlock.getChild(link.key); if (selContainsLink) change.unwrapInlineByKey(link.key); } change.deselect(); this.props.onBlur(); }); }; setFirstDocumentRef = ref => { this.firstDocument = ref; }; render() { const href = this.props.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ā€¦" onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> {this.isEditing && ( )} {this.isEditing ? : } {hasResults && ( {this.resultIds.map((id, index) => { const document = this.props.documents.getById(id); if (!document) return null; return ( index === 0 && this.setFirstDocumentRef(ref) } document={document} key={document.id} onClick={ev => this.selectDocument(ev, document)} /> ); })} )} ); } } const SearchResults = styled.div` background: #2f3336; position: absolute; top: 100%; width: 100%; height: auto; left: 0; padding: 8px; margin-top: -3px; margin-bottom: 0; border-radius: 0 0 4px 4px; `; const LinkEditor = styled(Flex)` margin-left: -8px; margin-right: -8px; `; const Input = styled.input` font-size: 15px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; padding: 4px 8px; border: 0; margin: 0; outline: none; color: #fff; flex-grow: 1; `; export default withRouter(inject('documents')(LinkToolbar));