Merge pull request #180 from jorilallo/image-uploads
Image Improvements
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<img
|
||||
<StyledImg
|
||||
{...attributes}
|
||||
src={node.data.get('src')}
|
||||
alt={node.data.get('alt')}
|
||||
src={src}
|
||||
alt={alt}
|
||||
active={active}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
56
frontend/components/Editor/insertImage.js
Normal file
56
frontend/components/Editor/insertImage.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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<any>,
|
||||
};
|
||||
|
||||
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<any>,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
15
yarn.lock
15
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:
|
||||
|
||||
Reference in New Issue
Block a user