diff --git a/.eslintrc b/.eslintrc index be1271ebd..a1eb5ce44 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,6 +18,13 @@ // Prettier automatically uses the least amount of parens possible, so this // does more harm than good. "no-mixed-operators": "off", + // Temporary fix for a failing import lint + "import/no-unresolved": [ + "error", + { + "ignore": [ "slate-drop-or-paste-images" ] + } + ], // Flow "flowtype/require-valid-file-annotation": [ 2, diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 946064ff8..258031285 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -6,13 +6,13 @@ import keydown from 'react-keydown'; import classnames from 'classnames/bind'; import type { Document, State, Editor as EditorType } from './types'; import getDataTransferFiles from 'utils/getDataTransferFiles'; -import uploadFile from 'utils/uploadFile'; import Flex from 'components/Flex'; import ClickablePadding from './components/ClickablePadding'; import Toolbar from './components/Toolbar'; import Markdown from './serializer'; import createSchema from './schema'; import createPlugins from './plugins'; +import insertImage from './insertImage'; import styled from 'styled-components'; import styles from './Editor.scss'; @@ -95,23 +95,22 @@ type KeyData = { const files = getDataTransferFiles(ev); for (const file of files) { - await this.insertFile(file); + await this.insertImageFile(file); } }; - insertFile = async (file: Object) => { - this.props.onImageUploadStart(); - const asset = await uploadFile(file); + insertImageFile = async (file: window.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() }); + let transform = state.transform(); + + transform = await insertImage( + transform, + file, + this.editor, + this.props.onImageUploadStart, + this.props.onImageUploadStop + ); + this.editor.onChange(transform.apply()); }; cancelEvent = (ev: SyntheticEvent) => { diff --git a/frontend/components/Editor/components/Image.js b/frontend/components/Editor/components/Image.js index d7841f37c..c98ee7a47 100644 --- a/frontend/components/Editor/components/Image.js +++ b/frontend/components/Editor/components/Image.js @@ -1,13 +1,27 @@ // @flow import React from 'react'; import type { Props } from '../types'; +import { color } from 'styles/constants'; +import styled from 'styled-components'; + +const StyledImg = styled.img` + box-shadow: ${props => (props.active ? `0 0 0 3px ${color.slate}` : '0')}; + opacity: ${props => (props.loading ? 0.5 : 1)}; +`; + +export default function Image({ attributes, state, node }: Props) { + const loading = node.data.get('loading'); + const alt = node.data.get('alt'); + const src = node.data.get('src'); + const active = state.isFocused && state.selection.hasEdgeIn(node); -export default function Image({ attributes, node }: Props) { return ( - {node.data.get('alt')} ); } diff --git a/frontend/components/Editor/insertImage.js b/frontend/components/Editor/insertImage.js new file mode 100644 index 000000000..43e56069a --- /dev/null +++ b/frontend/components/Editor/insertImage.js @@ -0,0 +1,56 @@ +// @flow +import uuid from 'uuid'; +import uploadFile from 'utils/uploadFile'; +import type { Editor, Transform } from './types'; + +export default async function insertImageFile( + transform: Transform, + file: window.File, + editor: Editor, + onImageUploadStart: () => void, + onImageUploadStop: () => void +) { + onImageUploadStart(); + + try { + // load the file as a data URL + const id = uuid.v4(); + const alt = file.name; + const reader = new FileReader(); + reader.addEventListener('load', () => { + const src = reader.result; + + // insert into document as uploading placeholder + const state = transform + .insertBlock({ + type: 'image', + isVoid: true, + data: { src, alt, id, loading: true }, + }) + .apply(); + editor.onChange(state); + }); + reader.readAsDataURL(file); + + // now we have a placeholder, start the upload + const asset = await uploadFile(file); + const src = asset.url; + + // we dont use the original transform provided to the callback here + // as the state may have changed significantly in the time it took to + // upload the file. + const state = editor.getState(); + const finalTransform = state.transform(); + const placeholder = state.document.findDescendant( + node => node.data && node.data.get('id') === id + ); + + return finalTransform.setNodeByKey(placeholder.key, { + data: { src, alt, loading: false }, + }); + } catch (err) { + throw err; + } finally { + onImageUploadStop(); + } +} diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index af9fd158b..d56bfcfaf 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -6,9 +6,9 @@ import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; import EditCode from 'slate-edit-code'; import Prism from 'slate-prism'; -import uploadFile from 'utils/uploadFile'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; +import insertImage from './insertImage'; const onlyInCode = node => node.type === 'code'; @@ -25,24 +25,14 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { }), DropOrPasteImages({ extensions: ['png', 'jpg', 'gif'], - applyTransform: async (transform, file) => { - onImageUploadStart(); - try { - const asset = await uploadFile(file); - const alt = file.name; - const src = asset.url; - - return transform.insertBlock({ - type: 'image', - isVoid: true, - data: { src, alt }, - }); - } catch (err) { - // TODO: Show a failure alert - console.error(err); - } finally { - onImageUploadStop(); - } + applyTransform: (transform, editor, file) => { + return insertImage( + transform, + file, + editor, + onImageUploadStart, + onImageUploadStop + ); }, }), EditList({ diff --git a/frontend/components/Editor/types.js b/frontend/components/Editor/types.js index 79cad5d49..0db4d68ae 100644 --- a/frontend/components/Editor/types.js +++ b/frontend/components/Editor/types.js @@ -1,5 +1,6 @@ // @flow import { List, Set, Map } from 'immutable'; +import { Selection } from 'slate'; export type NodeTransform = { addMarkByKey: Function, @@ -83,15 +84,6 @@ export type Block = Node & { export type Document = Node; -export type Props = { - node: Node, - parent?: Node, - attributes?: Object, - editor: Editor, - readOnly?: boolean, - children?: React$Element, -}; - export type State = { document: Document, selection: Selection, @@ -108,3 +100,13 @@ export type State = { transform: Function, isBlurred: Function, }; + +export type Props = { + node: Node, + parent?: Node, + attributes?: Object, + state: State, + editor: Editor, + readOnly?: boolean, + children?: React$Element, +}; diff --git a/package.json b/package.json index dfa1cd5f4..2384681f6 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "sequelize-encrypted": "0.1.0", "slate": "^0.19.30", "slate-collapse-on-escape": "^0.2.1", - "slate-drop-or-paste-images": "^0.5.0", + "slate-drop-or-paste-images": "tommoor/slate-drop-or-paste-images#dev", "slate-edit-code": "^0.10.2", "slate-edit-list": "^0.7.0", "slate-markdown-serializer": "tommoor/slate-markdown-serializer", diff --git a/yarn.lock b/yarn.lock index 810762be4..67384488e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2501,11 +2501,7 @@ emoji-name-map@1.1.2: iterate-object "^1.3.1" map-o "^2.0.1" -emoji-regex@^6.1.0: - version "6.4.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e" - -emoji-regex@^6.5.1: +emoji-regex@^6.1.0, emoji-regex@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" @@ -4850,6 +4846,10 @@ json-loader@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" +json-loader@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -8013,9 +8013,9 @@ slate-collapse-on-escape@^0.2.1: dependencies: to-pascal-case "^1.0.0" -slate-drop-or-paste-images@^0.5.0: +slate-drop-or-paste-images@tommoor/slate-drop-or-paste-images#dev: version "0.5.0" - resolved "https://registry.yarnpkg.com/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.5.0.tgz#c90367f9612f75abae0d1d6b8b2008108da02598" + resolved "https://codeload.github.com/tommoor/slate-drop-or-paste-images/tar.gz/935894631acb528b1eec6a8a1a78857da9d0d1da" dependencies: data-uri-to-blob "0.0.4" es6-promise "^4.0.5" @@ -8023,6 +8023,7 @@ slate-drop-or-paste-images@^0.5.0: is-data-uri "^0.1.0" is-image "^1.0.1" is-url "^1.2.2" + json-loader "^0.5.7" mime-types "^2.1.11" slate-edit-code@^0.10.2: