Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(web, web-react): FileUploader: show cropped image #DS-954 #1091

Merged
merged 5 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions apps/web-twig-demo/assets/scripts/file-uploader-meta-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved, import/extensions, no-unused-vars
import { FileUploader, Modal } from '@lmc-eu/spirit-web/src/js/index.esm';

window.addEventListener('DOMContentLoaded', () => {
let file;
const ModalElement = document.getElementById('example_modal_data');
const ModalInstance = new Modal(ModalElement);
const Content = ModalElement.querySelector('[data-example-content]');
const CancelButton = ModalElement.querySelector('[data-element="cancel"]');
const FileUploaderElement = document.getElementById('example_customMetaData');
const FileUploaderInstance = FileUploader.getInstance(FileUploaderElement);
let toggleMetaData = false; // allow toggling meta data between two different values when clicking on edit button

const isFileImage = (file) => file.type.split('/')[0] === 'image';

// callbacks

const updateQueueCallback = (key, file, meta) => {
const attachmentElement = document.querySelector('#FileUploaderListWithMetaData');
const fileName = FileUploaderInstance.getUpdatedFileName(file.name);
const metaInput = attachmentElement?.querySelector(`input[name="attachments_${fileName}_meta"]`);

// If we have metadata, we check if the input exists and if so we update its value else we create a new one
if (meta) {
if (metaInput) {
metaInput.value = JSON.stringify(meta);
} else {
const attachmentMetaInputElement = document.createElement('input');
attachmentMetaInputElement.setAttribute('type', 'hidden');
attachmentMetaInputElement.setAttribute('name', `attachments_${fileName}_meta`);
attachmentMetaInputElement.setAttribute('value', JSON.stringify(meta));
attachmentElement?.appendChild(attachmentMetaInputElement);
}
}

// If we do not have metadata, we remove the input
if (!meta) {
metaInput && metaInput.remove();
}
};

const callbackOnDismiss = (key) => {
document.querySelector(`input[name="attachments_${key}_meta"]`)?.remove();
};

// custom functions

const customAddToQueue = (file, meta) => {
if (isFileImage(file)) {
const reader = new FileReader();

reader.readAsDataURL(file);
reader.onloadend = () => {
const base64data = reader.result;
localStorage.setItem('image', base64data);
Content.innerHTML = `<img src="${base64data}" style="width: 100%; height: auto" alt="${file.name}" />`;
ModalInstance.show();
};
}

FileUploaderInstance.updateQueue(
FileUploaderInstance.getUpdatedFileName(file.name),
file,
meta,
updateQueueCallback,
);
};

const customEdit = (event) => {
const key = event.target.closest('li').id;
const newMeta = toggleMetaData
? { x: 30, y: 30, width: 150, height: 150 }
: { x: 22, y: 0, width: 110, height: 100 };
toggleMetaData = !toggleMetaData;
const file = FileUploaderInstance.getFileFromQueue(key).file;
FileUploaderInstance.updateQueue(key, file, newMeta, updateQueueCallback);
};
moduleFunctions.customEdit = customEdit;

// modal functions

const removeFromQueue = () => {
FileUploaderInstance.removeFromQueue(FileUploaderInstance.getUpdatedFileName(file.name));
cleanup();
};

const cleanup = () => {
ModalInstance.hide();
Content.innerHTML = '';
file = undefined;
};

CancelButton.addEventListener('click', removeFromQueue);

FileUploaderElement.addEventListener('queuedFile.fileUploader', (event) => {
file = event.currentFile;

customAddToQueue(file);
});

FileUploaderElement.addEventListener('unqueuedFile.fileUploader', (event) => {
callbackOnDismiss(event.currentFile);
});

FileUploaderElement.addEventListener('editFile.fileUploader', (event) => {
customEdit(event.currentFile);
});
});
1 change: 1 addition & 0 deletions apps/web-twig-demo/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Encore
*/
.addEntry('app', './assets/app.ts')
.addEntry('fileUploaderImagePreview', './assets/scripts/file-uploader-image-preview.ts')
.addEntry('fileUploaderMetaData', './assets/scripts/file-uploader-meta-data.ts')
.addEntry('formValidations', './assets/scripts/form-validations.ts')
.addEntry('tooltipDismissibleViaJS', './assets/scripts/tooltip-dismissible-via-js.ts')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export const FileUploaderWithModalImagePreview = (args) => {
imagePreview(file.name, file);
};

const attachmentComponent = ({ id, ...props }: SpiritFileUploaderAttachmentProps) => (
<FileUploaderAttachment key={id} id={id} onEdit={onEdit} {...props} />
const attachmentComponent = ({ id, meta, ...props }: SpiritFileUploaderAttachmentProps) => (
<FileUploaderAttachment key={id} id={id} meta={meta} onEdit={onEdit} {...props} />
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
);

return (
Expand Down Expand Up @@ -90,7 +90,6 @@ export const FileUploaderWithModalImagePreview = (args) => {
hasImagePreview
/>
</FileUploader>

<Modal id="ModalExample" isOpen={isModalOpen} onClose={handleClose}>
<ModalDialog>
<ModalBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import React from 'react';
import { useFileUploaderStyleProps } from './useFileUploaderStyleProps';
import { FileMetadata } from '../../types/fileUploader';
import { IMAGE_DIMENSION } from './constants';
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
import { useFileUploaderStyleProps } from './useFileUploaderStyleProps';

type AttachmentImagePreviewProps = {
label: string;
imagePreview: string;
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
label: string;
meta?: FileMetadata;
};

const AttachmentImagePreview = ({ label, imagePreview }: AttachmentImagePreviewProps) => {
const { classProps } = useFileUploaderStyleProps();
const AttachmentImagePreview = ({ label, imagePreview, meta }: AttachmentImagePreviewProps) => {
const { classProps } = useFileUploaderStyleProps({ meta });
const { imageCropStyles } = classProps;

return (
<span className={classProps.attachment.image}>
<img src={imagePreview} width={IMAGE_DIMENSION} height={IMAGE_DIMENSION} alt={label} />
<img src={imagePreview} width={IMAGE_DIMENSION} height={IMAGE_DIMENSION} alt={label} style={imageCropStyles} />
</span>
);
};

AttachmentImagePreview.defaultProps = {
meta: undefined,
};

export default AttachmentImagePreview;
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useRef, RefObject, MouseEvent, useState } from 'react';
import classNames from 'classnames';
import { SpiritFileUploaderAttachmentProps } from '../../types';
import React, { MouseEvent, RefObject, useRef, useState } from 'react';
import { useClassNamePrefix, useDeprecationMessage, useStyleProps } from '../../hooks';
import { useFileUploaderStyleProps } from './useFileUploaderStyleProps';
import { useFileUploaderAttachment } from './useFileUploaderAttachment';
import AttachmentImagePreview from './AttachmentImagePreview';
import { SpiritFileUploaderAttachmentProps } from '../../types';
import { Icon } from '../Icon';
import { DEFAULT_ICON_NAME, DEFAULT_BUTTON_LABEL, DEFAULT_EDIT_BUTTON_LABEL } from './constants';
import { image2Base64Preview } from './utils';
import AttachmentActionButton from './AttachmentActionButton';
import AttachmentDismissButton from './AttachmentDismissButton';
import AttachmentImagePreview from './AttachmentImagePreview';
import { DEFAULT_BUTTON_LABEL, DEFAULT_EDIT_BUTTON_LABEL, DEFAULT_ICON_NAME } from './constants';
import { useFileUploaderAttachment } from './useFileUploaderAttachment';
import { useFileUploaderStyleProps } from './useFileUploaderStyleProps';
import { image2Base64Preview } from './utils';

const FileUploaderAttachment = (props: SpiritFileUploaderAttachmentProps) => {
const {
Expand All @@ -28,6 +28,7 @@ const FileUploaderAttachment = (props: SpiritFileUploaderAttachmentProps) => {
onEdit,
onError,
removeText,
meta,
...restProps
} = props;
const [imagePreview, setImagePreview] = useState<string>('');
Expand All @@ -51,7 +52,7 @@ const FileUploaderAttachment = (props: SpiritFileUploaderAttachmentProps) => {
image2Base64Preview(file, 100, (compressedDataURL) => setImagePreview(compressedDataURL));
}

useFileUploaderAttachment({ attachmentRef, file, name, onError });
useFileUploaderAttachment({ attachmentRef, file, name, meta, onError });

useDeprecationMessage({
method: 'property',
Expand Down Expand Up @@ -82,7 +83,7 @@ const FileUploaderAttachment = (props: SpiritFileUploaderAttachmentProps) => {
className={classNames(classProps.attachment.root, styleProps.className)}
>
{hasImagePreview && imagePreview ? (
<AttachmentImagePreview label={label} imagePreview={imagePreview} />
<AttachmentImagePreview label={label} imagePreview={imagePreview} meta={meta} />
) : (
<Icon name={iconName} aria-hidden="true" />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ const FileUploaderList = (props: SpiritFileUploaderListProps) => {
const { fileQueue, onDismiss } = useFileUploaderContext();

const renderAttachments = useMemo(() => {
const fileArray = Array.from(fileQueue, (entry) => {
return { key: entry[0], file: entry[1] };
});
const fileArray = Array.from(fileQueue, (entry) => ({ key: entry[0], file: entry[1].file, meta: entry[1].meta }));

return fileArray.map(
({ key, file }) =>
({ key, file, meta }) =>
attachmentComponent &&
attachmentComponent({
id: key,
label: file.name,
name: inputName,
file,
meta,
onDismiss,
hasImagePreview,
}),
Expand Down
76 changes: 60 additions & 16 deletions packages/web-react/src/components/FileUploader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,24 +298,68 @@ const resetStateHandler = () => {
</form>;
```

### Passing additional metadata

When you need to send additional data along with the image you can do it with the `meta` argument on `addToQueue` and `updateQueue` callbacks.
If any data in `meta` option will be present, the FileUploader adds an additional hidden input with JSON stringified data to the form.
The identification of this input (`name`) will be the name of the file.
Thus you can pass to the server any additional text data you need.

```javascript
const customAddToQueue = (key: string, file: File) => {
// passing additional data using the `meta` argument
return addToQueue(key, file, { fileDescription: 'custom file description' });
};

const customUpdate = (_event: MouseEvent, file: File) => {
return updateQueue(file.name, file, { fileDescription: 'changing the custom description' });
};

// …
<FileUploader
addToQueue={customAddToQueue}
// …
updateQueue={updateQueue}
>
// …
</FileUploader>;
// …
```

#### Updating Image Preview with cropped image

When you are using FileUploader with some kind of image cropper you want to also update the image preview on FileUploaderAttachment when image changes.
You can do this by passing a specific object in shape of coordinates (`{ x: number, y: number, width: number, height: number }`) to the `meta` argument.
Then the coordinates will be applied to the preview image in the attachment.

```javascript
// …
const customUpdate = (_event: MouseEvent, file: File) => {
const meta = { x: 30, y: 30, width: 150, height: 150 };

return updateQueue(file.name, file, meta);
};
// …
```

## FileUploader Props

| Name | Type | Default | Required | Description |
| ------------------------------------- | ----------------------------------------------- | ------- | -------- | ------------------------------------------------------------------- |
| `addToQueue` | `(key: string, file: File) => FileQueueMapType` | — | ✔ | Callback to add an item to the queue |
| `clearQueue` | `() => void` | — | ✔ | Callback to clear the queue |
| `errorMessages.errorFileDuplicity` | `string` | — | ✕ | Translation for the error message: Duplicate file in queue |
| `errorMessages.errorMaxFileSize` | `string` | — | ✕ | Translation for the error message: Maximum file size |
| `errorMessages.errorMaxUploadedFiles` | `string` | — | ✕ | Translation for the error message: Maximum number of uploaded files |
| `fileQueue` | `FileQueueMapType` | — | ✔ | Queue of items to upload |
| `findInQueue` | `(key: string) => FileQueueMapType` | — | ✔ | A callback to find a particular item in the queue |
| `id` | `string` | — | ✔ | FileUploader id |
| `isDisabled` | `bool` | — | ✕ | When the field is supposed to be disabled |
| `isFluid` | `bool` | — | ✕ | When the field is supposed to be fluid |
| `onDismiss` | `(key: string) => FileQueueMapType` | — | ✔ | A callback to delete a particular item from the queue |
| `UNSAFE_className` | `string` | — | ✕ | FileUploader custom class name |
| `UNSAFE_style` | `CSSProperties` | — | ✕ | FileUploader custom style |
| `updateQueue` | `(key: string, file: File) => FileQueueMapType` | — | ✔ | A callback to update a particular item in the queue |
| Name | Type | Default | Required | Description |
| ------------------------------------- | -------------------------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------- |
| `addToQueue` | `(key: string, file: File, meta?: FileMetadata) => FileQueueMapType` | — | ✔ | Callback to add an item to the queue |
| `clearQueue` | `() => void` | — | ✔ | Callback to clear the queue |
| `errorMessages.errorFileDuplicity` | `string` | — | ✕ | Translation for the error message: Duplicate file in queue |
| `errorMessages.errorMaxFileSize` | `string` | — | ✕ | Translation for the error message: Maximum file size |
| `errorMessages.errorMaxUploadedFiles` | `string` | — | ✕ | Translation for the error message: Maximum number of uploaded files |
| `fileQueue` | `FileQueueMapType` | — | ✔ | Queue of items to upload |
| `findInQueue` | `(key: string) => FileQueueMapType` | — | ✔ | A callback to find a particular item in the queue |
| `id` | `string` | — | ✔ | FileUploader id |
| `isDisabled` | `bool` | — | ✕ | When the field is supposed to be disabled |
| `isFluid` | `bool` | — | ✕ | When the field is supposed to be fluid |
| `onDismiss` | `(key: string) => FileQueueMapType` | — | ✔ | A callback to delete a particular item from the queue |
| `UNSAFE_className` | `string` | — | ✕ | FileUploader custom class name |
| `UNSAFE_style` | `CSSProperties` | — | ✕ | FileUploader custom style |
| `updateQueue` | `(key: string, file: File, meta?: FileMetadata) => FileQueueMapType` | — | ✔ | A callback to update a particular item in the queue |

The rest of the properties are created from the default `<div>` element. [More about the element][DivElementDocs]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe('FileUploaderList', () => {
it('should render the attachments', () => {
const attachmentComponent = (ps: SpiritFileUploaderAttachmentProps) => <li key={ps.id}>{ps.label}</li>;
const fileQueue = new Map();
fileQueue.set('1', { name: 'file1.txt' });
fileQueue.set('2', { name: 'file2.txt' });
fileQueue.set('1', { file: { name: 'file1.txt' } });
fileQueue.set('2', { file: { name: 'file2.txt' } });
const { getByText } = render(
<FileUploader
fileQueue={fileQueue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,27 @@ describe('useFileQueue', () => {

it('should update queue', () => {
const { result } = renderHook(() => useFileQueue());
const testMeta = { test: 'test' };
const testUpdateMeta = { test: 'test updated' };

act(() => {
result.current.addToQueue('test1_txt', file1);
result.current.addToQueue('test2_txt', file2);
result.current.addToQueue('test3_txt', file2, testMeta);
});

expect(result.current.fileQueue.size).toBe(2);
expect(result.current.fileQueue.size).toBe(3);
expect(result.current.fileQueue.get('test3_txt')).toEqual({ file: file2, meta: testMeta });

act(() => {
result.current.updateQueue('test1_txt', file2);
result.current.updateQueue('test2_txt', file1);
result.current.updateQueue('test3_txt', file2, testUpdateMeta);
});

expect(result.current.fileQueue.size).toBe(2);
expect(result.current.fileQueue.get('test1_txt')).toBe(file2);
expect(result.current.fileQueue.get('test2_txt')).toBe(file1);
expect(result.current.fileQueue.size).toBe(3);
expect(result.current.fileQueue.get('test1_txt')).toEqual({ file: file2 });
expect(result.current.fileQueue.get('test2_txt')).toEqual({ file: file1 });
expect(result.current.fileQueue.get('test3_txt')).toEqual({ file: file2, meta: testUpdateMeta });
});
});
Loading