Skip to content
This repository has been archived by the owner on Oct 25, 2022. It is now read-only.

Commit

Permalink
Added image upload dropzone
Browse files Browse the repository at this point in the history
  • Loading branch information
tiberiuichim committed Jul 18, 2020
1 parent 1710b35 commit 1c5d750
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 39 deletions.
171 changes: 139 additions & 32 deletions src/TextBlock/TextBlockEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,29 @@
* especially the list element.
*/

import React, { useMemo, useState } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'semantic-ui-react';
import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
import { readAsDataURL } from 'promise-file-reader';

import { Icon, BlockChooser, SidebarPortal } from '@plone/volto/components';
import { uploadContent } from 'volto-slate/actions';
import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
import addSVG from '@plone/volto/icons/circle-plus.svg';
import {
flattenToAppURL,
getBaseUrl,
isInternalURL,
} from '@plone/volto/helpers';
import { settings } from '~/config';

import { saveSlateBlockSelection } from 'volto-slate/actions';
import SlateEditor from 'volto-slate/editor';
import { serializeNodesToText } from 'volto-slate/editor/render';
import ShortcutListing from './ShortcutListing';
import { handleKey } from './keyboard';
import Dropzone from 'react-dropzone';
import './styles.css';

// import { withList, withDeserializeHtml } from './extensions';
// import {
Expand All @@ -34,16 +44,24 @@ const TextBlockEdit = (props) => {
onChangeBlock,
onMutateBlock,
onSelectBlock,
pathname,
properties,
selected,
uploadRequest,
uploadContent,
uploadedContent,
} = props;

// console.log('properties', properties);

const { slate } = settings;
const { textblockExtensions } = slate;
const { value } = data;
const [addNewBlockOpened, setAddNewBlockOpened] = useState();

const [addNewBlockOpened, setAddNewBlockOpened] = React.useState();
const [showDropzone, setShowDropzone] = React.useState(false);
const [uploading, setUploading] = React.useState(false);
const [newImageId, setNewImageId] = React.useState(null);

const prevReq = React.useRef(null);

// const keyDownHandlers = useMemo(() => {
// return {
Expand Down Expand Up @@ -106,36 +124,116 @@ const TextBlockEdit = (props) => {
[props],
);

const onDrop = React.useCallback(
(files) => {
setUploading(true);
const file = files[0];
readAsDataURL(file).then((data) => {
const fields = data.match(/^data:(.*);(.*),(.*)$/);
uploadContent(
getBaseUrl(pathname),
{
'@type': 'Image',
title: file.name,
image: {
data: fields[3],
encoding: fields[2],
'content-type': fields[1],
filename: file.name,
},
},
block,
);
});
setShowDropzone(false);
},
[pathname, uploadContent, block],
);

const { loaded, loading } = uploadRequest;
const imageId = uploadedContent['@id'];
const prevLoaded = prevReq.current;

React.useLayoutEffect(() => {
if (loaded && !loading && !prevLoaded && newImageId !== imageId) {
const url = flattenToAppURL(imageId);
setNewImageId(imageId);
setTimeout(() => {
const id = onAddBlock('image', index + 1);
const options = {
'@type': 'image',
url,
};
onChangeBlock(id, options);
}, 0);
}
prevReq.current = loaded;
}, [
loaded,
loading,
prevLoaded,
imageId,
newImageId,
index,
onChangeBlock,
onAddBlock,
]);

return (
<>
<SidebarPortal selected={selected}>
<div id="slate-plugin-sidebar"></div>
<ShortcutListing />
</SidebarPortal>
<SlateEditor
index={index}
properties={properties}
onAddBlock={onAddBlock}
extensions={[withBlockProperties, ...textblockExtensions]}
onSelectBlock={onSelectBlock}
value={value}
block={block}
onChange={(value, selection) => {
// without using setTimeout, the user types characters on the right side of the text cursor
// timeoutTillRerender = setTimeout(() => {
// saveSlateBlockSelection(block, selection);
// });

onChangeBlock(block, {
...data,
value,
plaintext: serializeNodesToText(value || []),
});
}}
onKeyDown={handleKey}
selected={selected}
placeholder="Enter some rich text…"
/>

<Dropzone
disableClick
onDrop={onDrop}
className="dropzone"
onDragOver={() => setShowDropzone(true)}
onDragLeave={() => setShowDropzone(false)}
>
{showDropzone ? (
<div className="drop-indicator">
{uploading ? (
<Dimmer active>
<Loader indeterminate>Uploading image</Loader>
</Dimmer>
) : (
<Message>
<center>
<img src={imageBlockSVG} alt="" />
</center>
</Message>
)}
</div>
) : (
<SlateEditor
index={index}
properties={properties}
onAddBlock={onAddBlock}
extensions={[withBlockProperties, ...textblockExtensions]}
onSelectBlock={onSelectBlock}
value={value}
block={block}
onChange={(value, selection) => {
// without using setTimeout, the user types characters on the right side of the text cursor
// timeoutTillRerender = setTimeout(() => {
// saveSlateBlockSelection(block, selection);
// });

onChangeBlock(block, {
...data,
value,
plaintext: serializeNodesToText(value || []),
});
}}
onKeyDown={handleKey}
selected={selected}
placeholder="Enter some rich text…"
/>
)}
</Dropzone>
{!detached && !data.plaintext && (
<Button
basic
Expand All @@ -153,6 +251,15 @@ const TextBlockEdit = (props) => {
);
};

export default connect(null, {
saveSlateBlockSelection, // needed to dispatch action in keyboard handlers
})(TextBlockEdit);
export default connect(
(state, props) => {
return {
uploadRequest: state.upload_content[props.block]?.upload || {},
uploadedContent: state.upload_content[props.block]?.data || {},
};
},
{
uploadContent,
saveSlateBlockSelection, // needed as editor blockProps
},
)(TextBlockEdit);
4 changes: 4 additions & 0 deletions src/TextBlock/keyboard/indentListItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function decreaseItemDepth(editor, event) {

// Current list item being unindented
const [listItemNode, listItemPath] = getCurrentListItem(editor);

// The ul/ol that holds the current list item
const [, parentListPath] = Editor.parent(editor, listItemPath);

Expand All @@ -108,6 +109,9 @@ export function decreaseItemDepth(editor, event) {
return true;
}

// TODO: when unindenting a sublist item, it should take its next siblings
// with it as a sublist

// Get the parent list item for the parent
const [, parentListItemPath] = Editor.parent(editor, parentListPath);

Expand Down
18 changes: 18 additions & 0 deletions src/TextBlock/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.drop-indicator {
width: 100%;
min-height: 6rem;
}

.drop-indicator .ui.message {
/* padding: 0px; */
}

.drop-indicator .ui.message center {
height: 100%;
/* margin: 0 auto; */
}

.drop-indicator .ui.message center img {
width: 100%;
height: 100%;
}
13 changes: 13 additions & 0 deletions src/actions/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { nestContent } from '@plone/volto/helpers';
import { UPLOAD_CONTENT } from 'volto-slate/constants';

// A custom version of Volto's createContent that can take an "origin" block id
export function uploadContent(url, content, origin) {
return {
type: UPLOAD_CONTENT,
origin,
request: Array.isArray(content)
? content.map((item) => ({ op: 'post', path: url, data: item }))
: { op: 'post', path: url, data: nestContent(content) },
};
}
1 change: 1 addition & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export saveSlateBlockSelection from './selection';
export * from './content';
4 changes: 3 additions & 1 deletion src/actions/selection.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SAVE_SLATE_BLOCK_SELECTION } from 'volto-slate/constants';

export default function saveSlateBlockSelection(blockid, selection) {
return {
type: 'SAVE_SLATE_BLOCK_SELECTION',
type: SAVE_SLATE_BLOCK_SELECTION,
blockid,
selection,
};
Expand Down
7 changes: 2 additions & 5 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export const LISTTYPES = ['bulleted-list', 'numbered-list'];

export default {
LISTTYPES,
};
export const SAVE_SLATE_BLOCK_SELECTION = 'SAVE_SLATE_BLOCK_SELECTION';
export const UPLOAD_CONTENT = 'UPLOAD_CONTENT';
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import codeSVG from '@plone/volto/icons/code.svg';

import { slate_block_selections } from './reducers';
import { slate_block_selections, upload_content } from './reducers';

import installVoltoProposals from './futurevolto/config';

Expand Down Expand Up @@ -90,6 +90,7 @@ const applyConfig = (config) => {
config.addonReducers = {
...config.addonReducers,
slate_block_selections,
upload_content,
};

config.views = {
Expand Down
74 changes: 74 additions & 0 deletions src/reducers/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Upload reducer.
* @module reducers/content
*
* Customized copy of Volto's content reducer
*/

import { settings } from '~/config';

import { UPLOAD_CONTENT } from 'volto-slate/constants';

const initialState = {};

/**
* Content reducer.
* @function content
* @param {Object} state Current state.
* @param {Object} action Action to be handled.
* @returns {Object} New state.
*/
export default function upload_content(state = initialState, action = {}) {
let { result, origin } = action;
switch (action.type) {
case `${UPLOAD_CONTENT}_PENDING`:
return {
...state,
[origin]: {
...state[origin],
upload: {
loading: true,
loaded: false,
error: null,
},
},
};
case `${UPLOAD_CONTENT}_SUCCESS`:
return {
...state,
[origin]: {
...state[origin],
data: {
...result,
items:
action.result &&
action.result.items &&
action.result.items.map((item) => ({
...item,
url: item['@id'].replace(settings.apiPath, ''),
})),
},
upload: {
loading: false,
loaded: true,
error: null,
},
},
};
case `${UPLOAD_CONTENT}_FAIL`:
return {
...state,
[origin]: {
...state[origin],
data: null,
upload: {
loading: false,
loaded: false,
error: action.error,
},
},
};
default:
return state;
}
}
1 change: 1 addition & 0 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export slate_block_selections from './selection';
export upload_content from './content';

0 comments on commit 1c5d750

Please sign in to comment.