From 667d1caf11930b6f04b3be61c630c7f48e8838b2 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 26 Aug 2017 22:37:20 -0700 Subject: [PATCH 1/4] Added: Placeholder when uploading images --- .../components/Editor/components/Image.js | 17 ++--- frontend/components/Editor/plugins.js | 28 +-------- .../components/Editor/plugins/ImageUploads.js | 63 +++++++++++++++++++ package.json | 2 +- yarn.lock | 10 +-- 5 files changed, 80 insertions(+), 40 deletions(-) create mode 100644 frontend/components/Editor/plugins/ImageUploads.js diff --git a/frontend/components/Editor/components/Image.js b/frontend/components/Editor/components/Image.js index d7841f37c..aacbf36b2 100644 --- a/frontend/components/Editor/components/Image.js +++ b/frontend/components/Editor/components/Image.js @@ -1,13 +1,16 @@ // @flow import React from 'react'; import type { Props } from '../types'; +import styled from 'styled-components'; + +const LoadingImage = styled.img` + opacity: .5; +`; export default function Image({ attributes, node }: Props) { - return ( - {node.data.get('alt')} - ); + const loading = node.data.get('loading'); + const Component = loading ? LoadingImage : 'img'; + const src = node.data.get('inlineSrc') || node.data.get('src'); + + return ; } diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index af9fd158b..dfd0d1559 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -1,14 +1,13 @@ // @flow -import DropOrPasteImages from 'slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import EditList from 'slate-edit-list'; 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 ImageUploads from './plugins/ImageUploads'; const onlyInCode = node => node.type === 'code'; @@ -17,34 +16,13 @@ type Options = { onImageUploadStop: Function, }; -const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { +const createPlugins = (options: Options) => { return [ PasteLinkify({ type: 'link', collapseTo: 'end', }), - 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(); - } - }, - }), + ImageUploads(options), EditList({ types: ['ordered-list', 'bulleted-list', 'todo-list'], typeItem: 'list-item', diff --git a/frontend/components/Editor/plugins/ImageUploads.js b/frontend/components/Editor/plugins/ImageUploads.js new file mode 100644 index 000000000..e7967f616 --- /dev/null +++ b/frontend/components/Editor/plugins/ImageUploads.js @@ -0,0 +1,63 @@ +// @flow +import uuid from 'uuid'; +import DropOrPasteImages from 'slate-drop-or-paste-images'; +import uploadFile from 'utils/uploadFile'; + +type Options = { + onImageUploadStart: Function, + onImageUploadStop: Function, +}; + +export default function ImageUploads({ + onImageUploadStart, + onImageUploadStop, +}: Options) { + return DropOrPasteImages({ + extensions: ['png', 'jpg', 'gif'], + applyTransform: async (transform, editor, file) => { + onImageUploadStart(); + + // 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 + try { + 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 transform = state.transform(); + const placeholder = state.document.findDescendant( + node => node.data && node.data.get('id') === id + ); + return transform.setNodeByKey(placeholder.key, { + data: { src, alt, loading: false }, + }); + } catch (err) { + // TODO: Show a failure alert + console.error(err); + } finally { + onImageUploadStop(); + } + }, + }); +} diff --git a/package.json b/package.json index dfa1cd5f4..8ddc39b45 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", "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..1f8073b6c 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" @@ -8013,9 +8009,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: 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/a3ebc74658941ec2c56d4b90493b8718b8a9fd4f" dependencies: data-uri-to-blob "0.0.4" es6-promise "^4.0.5" From 3a348d7b8b2d6c7b587954830bfab74ddca3ebba Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 27 Aug 2017 09:03:05 -0700 Subject: [PATCH 2/4] Update to use dev branch --- frontend/components/Editor/Editor.js | 27 ++++---- frontend/components/Editor/insertImage.js | 56 +++++++++++++++++ frontend/components/Editor/plugins.js | 18 +++++- .../components/Editor/plugins/ImageUploads.js | 63 ------------------- package.json | 2 +- yarn.lock | 9 ++- 6 files changed, 92 insertions(+), 83 deletions(-) create mode 100644 frontend/components/Editor/insertImage.js delete mode 100644 frontend/components/Editor/plugins/ImageUploads.js 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/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 dfd0d1559..d56bfcfaf 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -1,4 +1,5 @@ // @flow +import DropOrPasteImages from 'slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import EditList from 'slate-edit-list'; import CollapseOnEscape from 'slate-collapse-on-escape'; @@ -7,7 +8,7 @@ import EditCode from 'slate-edit-code'; import Prism from 'slate-prism'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; -import ImageUploads from './plugins/ImageUploads'; +import insertImage from './insertImage'; const onlyInCode = node => node.type === 'code'; @@ -16,13 +17,24 @@ type Options = { onImageUploadStop: Function, }; -const createPlugins = (options: Options) => { +const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { return [ PasteLinkify({ type: 'link', collapseTo: 'end', }), - ImageUploads(options), + DropOrPasteImages({ + extensions: ['png', 'jpg', 'gif'], + applyTransform: (transform, editor, file) => { + return insertImage( + transform, + file, + editor, + onImageUploadStart, + onImageUploadStop + ); + }, + }), EditList({ types: ['ordered-list', 'bulleted-list', 'todo-list'], typeItem: 'list-item', diff --git a/frontend/components/Editor/plugins/ImageUploads.js b/frontend/components/Editor/plugins/ImageUploads.js deleted file mode 100644 index e7967f616..000000000 --- a/frontend/components/Editor/plugins/ImageUploads.js +++ /dev/null @@ -1,63 +0,0 @@ -// @flow -import uuid from 'uuid'; -import DropOrPasteImages from 'slate-drop-or-paste-images'; -import uploadFile from 'utils/uploadFile'; - -type Options = { - onImageUploadStart: Function, - onImageUploadStop: Function, -}; - -export default function ImageUploads({ - onImageUploadStart, - onImageUploadStop, -}: Options) { - return DropOrPasteImages({ - extensions: ['png', 'jpg', 'gif'], - applyTransform: async (transform, editor, file) => { - onImageUploadStart(); - - // 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 - try { - 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 transform = state.transform(); - const placeholder = state.document.findDescendant( - node => node.data && node.data.get('id') === id - ); - return transform.setNodeByKey(placeholder.key, { - data: { src, alt, loading: false }, - }); - } catch (err) { - // TODO: Show a failure alert - console.error(err); - } finally { - onImageUploadStop(); - } - }, - }); -} diff --git a/package.json b/package.json index 8ddc39b45..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": "tommoor/slate-drop-or-paste-images", + "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 1f8073b6c..67384488e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4846,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" @@ -8009,9 +8013,9 @@ slate-collapse-on-escape@^0.2.1: dependencies: to-pascal-case "^1.0.0" -slate-drop-or-paste-images@tommoor/slate-drop-or-paste-images: +slate-drop-or-paste-images@tommoor/slate-drop-or-paste-images#dev: version "0.5.0" - resolved "https://codeload.github.com/tommoor/slate-drop-or-paste-images/tar.gz/a3ebc74658941ec2c56d4b90493b8718b8a9fd4f" + 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" @@ -8019,6 +8023,7 @@ slate-drop-or-paste-images@tommoor/slate-drop-or-paste-images: 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: From 5f514f559ccb982a2f69d0bf88d8000b89b55037 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 27 Aug 2017 09:51:37 -0700 Subject: [PATCH 3/4] Show selected state --- .../components/Editor/components/Image.js | 23 ++++++++++++++----- frontend/components/Editor/types.js | 20 ++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/frontend/components/Editor/components/Image.js b/frontend/components/Editor/components/Image.js index aacbf36b2..c98ee7a47 100644 --- a/frontend/components/Editor/components/Image.js +++ b/frontend/components/Editor/components/Image.js @@ -1,16 +1,27 @@ // @flow import React from 'react'; import type { Props } from '../types'; +import { color } from 'styles/constants'; import styled from 'styled-components'; -const LoadingImage = styled.img` - opacity: .5; +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, node }: Props) { +export default function Image({ attributes, state, node }: Props) { const loading = node.data.get('loading'); - const Component = loading ? LoadingImage : 'img'; - const src = node.data.get('inlineSrc') || node.data.get('src'); + const alt = node.data.get('alt'); + const src = node.data.get('src'); + const active = state.isFocused && state.selection.hasEdgeIn(node); - return ; + return ( + + ); } 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, +}; From 795853cccba522ec799f954e02edffe816915383 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 28 Aug 2017 21:02:11 -0700 Subject: [PATCH 4/4] Exclude failing import --- .eslintrc | 7 +++++++ 1 file changed, 7 insertions(+) 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,