diff --git a/frontend/components/DropToImport/DropToImport.js b/frontend/components/DropToImport/DropToImport.js index b5a84100c..84b4cb163 100644 --- a/frontend/components/DropToImport/DropToImport.js +++ b/frontend/components/DropToImport/DropToImport.js @@ -19,6 +19,7 @@ class DropToImport extends Component { activeClassName?: string, rejectClassName?: string, documents: DocumentsStore, + disabled: boolean, history: Object, }; state = { @@ -83,9 +84,12 @@ class DropToImport extends Component { 'history', 'documentId', 'collectionId', - 'documents' + 'documents', + 'disabled' ); + if (this.props.disabled) return this.props.children; + return ( { + // check if this event was already handled by the Editor + if (ev.isDefaultPrevented()) return; + + // otherwise we'll handle this + ev.preventDefault(); + ev.stopPropagation(); + + const files = getDataTransferFiles(ev); + for (const file of files) { + await this.insertFile(file); + } + }; + + insertFile = async (file: Object) => { + this.props.onImageUploadStart(); + const asset = await uploadFile(file); + const state = this.editor.getState(); + const transform = state.transform(); + transform.collapseToEndOf(state.document); + transform.insertBlock({ + type: 'image', + isVoid: true, + data: { src: asset.url, alt: file.name }, + }); + this.props.onImageUploadStop(); + this.setState({ state: transform.apply() }); + }; + + cancelEvent = (ev: SyntheticEvent) => { + ev.preventDefault(); + }; + + // Handling of keyboard shortcuts outside of editor focus + @keydown('meta+s') + onSave(ev: SyntheticKeyboardEvent) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onSave(); + } + + @keydown('meta+enter') + onSaveAndExit(ev: SyntheticKeyboardEvent) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onSave({ redirect: false }); + } + + @keydown('esc') + onCancel() { + this.props.onCancel(); + } + + // Handling of keyboard shortcuts within editor focus onKeyDown = (ev: SyntheticKeyboardEvent, data: KeyData, state: State) => { if (!data.isMeta) return; switch (data.key) { case 's': - ev.preventDefault(); - ev.stopPropagation(); - this.props.onSave(); - return state; + this.onSave(ev); + break; case 'enter': - ev.preventDefault(); - ev.stopPropagation(); - this.props.onSave({ redirect: false }); - return state; + this.onSaveAndExit(ev); + break; case 'escape': - return this.props.onCancel(); + this.onCancel(); + break; default: } }; @@ -120,31 +174,40 @@ type KeyData = { render = () => { return ( - - - {this.props.heading} - - - (this.editor = ref)} - placeholder="Start with a title..." - className={cx(styles.editor, { readOnly: this.props.readOnly })} - schema={this.schema} - plugins={this.plugins} - state={this.state.state} - onChange={this.onChange} - onDocumentChange={this.onDocumentChange} - onKeyDown={this.onKeyDown} - onSave={this.props.onSave} - readOnly={this.props.readOnly} - /> - {!this.props.readOnly && - } - + + + + {this.props.heading} + + + (this.editor = ref)} + placeholder="Start with a title..." + className={cx(styles.editor, { readOnly: this.props.readOnly })} + schema={this.schema} + plugins={this.plugins} + state={this.state.state} + onKeyDown={this.onKeyDown} + onChange={this.onChange} + onDocumentChange={this.onDocumentChange} + onSave={this.props.onSave} + readOnly={this.props.readOnly} + /> + {!this.props.readOnly && + } + + ); }; } @@ -153,7 +216,8 @@ MarkdownEditor.childContextTypes = { starred: PropTypes.bool, }; -const Container = styled(Flex)` +const MaxWidth = styled(Flex)` + max-width: 50em; height: 100%; `; diff --git a/frontend/components/Editor/Editor.scss b/frontend/components/Editor/Editor.scss index 9cb57c6d5..808cd0f01 100644 --- a/frontend/components/Editor/Editor.scss +++ b/frontend/components/Editor/Editor.scss @@ -1,6 +1,9 @@ .editor { - color: #1b2631; + font-weight: 400; + font-size: 1em; + line-height: 1.5em; width: 100%; + color: #1b2631; h1, h2, diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index b8912a1ea..af9fd158b 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -26,8 +26,8 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { DropOrPasteImages({ extensions: ['png', 'jpg', 'gif'], applyTransform: async (transform, file) => { + onImageUploadStart(); try { - onImageUploadStart(); const asset = await uploadFile(file); const alt = file.name; const src = asset.url; @@ -39,6 +39,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { }); } catch (err) { // TODO: Show a failure alert + console.error(err); } finally { onImageUploadStop(); } diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 96e7ccacc..001d4d961 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -9,6 +9,7 @@ import _ from 'lodash'; import keydown from 'react-keydown'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; +import { documentEditUrl, homeUrl, searchUrl } from 'utils/routeHelpers'; import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; @@ -51,15 +52,21 @@ type Props = { @observable modal = null; @keydown(['/', 't']) - search() { - if (this.props.auth.authenticated) - _.defer(() => this.props.history.push('/search')); + goToSearch(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.history.push(searchUrl()); } @keydown('d') - dashboard() { - if (this.props.auth.authenticated) - _.defer(() => this.props.history.push('/')); + goToDashboard() { + this.props.history.push(homeUrl()); + } + + @keydown('e') + goToEdit() { + if (!this.props.ui.activeDocument) return; + this.props.history.push(documentEditUrl(this.props.ui.activeDocument)); } handleLogout = () => { @@ -67,9 +74,9 @@ type Props = { }; @keydown('shift+/') - handleOpenKeyboardShortcuts = () => { + handleOpenKeyboardShortcuts() { this.modal = 'keyboard-shortcuts'; - }; + } handleCreateCollection = () => { this.modal = 'create-collection'; diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index ec5502191..08f589e14 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -124,13 +124,13 @@ type Props = { ); } - onImageUploadStart() { + onImageUploadStart = () => { this.setState({ isLoading: true }); - } + }; - onImageUploadStop() { + onImageUploadStop = () => { this.setState({ isLoading: false }); - } + }; onChange = text => { if (!this.document) return; @@ -168,7 +168,7 @@ type Props = { render() { const isNew = this.props.newDocument; - const isEditing = this.props.match.params.edit || isNew; + const isEditing = !!this.props.match.params.edit || isNew; const isFetching = !this.document; const titleText = get(this.document, 'title', 'Loading'); @@ -192,28 +192,27 @@ type Props = { onDragEnter={this.onStartDragging} onDragLeave={this.onStopDragging} onDrop={this.onStopDragging} + disabled={isEditing} > - - - + @@ -270,14 +269,6 @@ const LoadingState = styled(PreviewLoading)` margin: 80px 20px; `; -const DocumentContainer = styled.div` - font-weight: 400; - font-size: 1em; - line-height: 1.5em; - padding: 0 3em; - width: 50em; -`; - const StyledDropToImport = styled(DropToImport)` display: flex; flex: 1; diff --git a/frontend/static/flatpages/keyboard.md b/frontend/static/flatpages/keyboard.md index 4855c385f..8d0eeeb02 100644 --- a/frontend/static/flatpages/keyboard.md +++ b/frontend/static/flatpages/keyboard.md @@ -1,5 +1,5 @@ - `Cmd+Enter` - Save and exit document editor -- `Cmd+s` - Save document and continue editing +- `Cmd+S` - Save document and continue editing - `Cmd+Esc` - Cancel edit - `/` or `t` - Jump to search - `d` - Jump to dashboard diff --git a/frontend/utils/getDataTransferFiles.js b/frontend/utils/getDataTransferFiles.js new file mode 100644 index 000000000..5811090be --- /dev/null +++ b/frontend/utils/getDataTransferFiles.js @@ -0,0 +1,18 @@ +// @flow +export default function getDataTransferFiles(event: SyntheticEvent) { + let dataTransferItemsList = []; + if (event.dataTransfer) { + const dt = event.dataTransfer; + if (dt.files && dt.files.length) { + dataTransferItemsList = dt.files; + } else if (dt.items && dt.items.length) { + // During the drag even the dataTransfer.files is null + // but Chrome implements some drag store, which is accesible via dataTransfer.items + dataTransferItemsList = dt.items; + } + } else if (event.target && event.target.files) { + dataTransferItemsList = event.target.files; + } + // Convert from DataTransferItemsList to the native Array + return Array.prototype.slice.call(dataTransferItemsList); +} diff --git a/frontend/utils/routeHelpers.js b/frontend/utils/routeHelpers.js index 456150418..8c43c22dd 100644 --- a/frontend/utils/routeHelpers.js +++ b/frontend/utils/routeHelpers.js @@ -1,4 +1,5 @@ // @flow +import Document from 'models/Document'; export function homeUrl(): string { return '/dashboard'; @@ -12,7 +13,15 @@ export function newCollectionUrl(): string { return '/collections/new'; } -export function searchUrl(query: string): string { +export function documentUrl(doc: Document): string { + return doc.url; +} + +export function documentEditUrl(doc: Document): string { + return `${doc.url}/edit`; +} + +export function searchUrl(query?: string): string { if (query) return `/search/${query}`; return `/search`; }