docbrown/app/javascript/article-form/elements/imageManagement.jsx
Alex bc8c126497
Validate file uploads client-side (#6737)
* JavaScript MVP

- First round of linting
- Use for testing

* Add some default UI error handling

* Add file validation to updating profile image

* Add type validation and fix size bug

* Refactor and add file validation to article cover

* Add file validation to body images

* Add accept field to profile image form

* Add parseInt to maxFileSizeMb

* Add accept and max-file-size-mb to article images

* Change parseInt to Number

* Add file validations to podcast form

* Add accept image to podcasts

* Add file validation and accept field to org form

* Use appendChild for error message

* Add file validation to comment form

* Add JS Docs comments

* Remove redundant format check

* Add documentation to override default values

* Update documention (spelling, formatting, etc.)

* Update max-file-size-mb to 25

* Bump default max file size for images to 25

* Change appendChild to append

* Refactor remove error message

* Refactor JS, remove console logs, update error msg

* Remove video from permitted file types

* Update error messaging

* Update docs for funciton params...oops 🙈

* Fix file size check logic

- Allow for 0 maxFileSizeMb
- Use JSON.parse() for permittedFileTypes to get an Array

* Add specs

* Change blackbox to black box

* Remove wait_for_javascript

* Remove comments from spec
2020-03-24 09:21:49 +01:00

245 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import linkCopyIcon from '../../../assets/images/content-copy.svg';
import { generateMainImage } from '../actions';
import { validateFileInputs } from '../../packs/validateFileInputs';
export default class ImageManagement extends Component {
constructor(props) {
super(props);
this.state = {
insertionImageUrls: [],
uploadError: false,
uploadErrorMessage: null,
};
}
handleMainImageUpload = e => {
e.preventDefault();
this.clearUploadError();
const validFileInputs = validateFileInputs();
if (validFileInputs) {
const payload = { image: e.target.files, wrap_cloudinary: true };
const { onMainImageUrlChange } = this.props;
generateMainImage(payload, onMainImageUrlChange, this.onUploadError);
}
};
handleInsertionImageUpload = e => {
this.clearUploadError();
const validFileInputs = validateFileInputs();
if (validFileInputs) {
const payload = { image: e.target.files };
generateMainImage(
payload,
this.handleInsertImageUploadSuccess,
this.onUploadError,
);
}
};
handleInsertImageUploadSuccess = response => {
this.setState({
insertionImageUrls: response.links,
});
};
triggerMainImageRemoval = e => {
e.preventDefault();
const { onMainImageUrlChange } = this.props;
onMainImageUrlChange({
links: [],
});
};
clearUploadError = () => {
this.setState({
uploadError: false,
uploadErrorMessage: null,
});
};
onUploadError = error => {
this.setState({
insertionImageUrls: [],
uploadError: true,
uploadErrorMessage: error.message,
});
};
copyText = () => {
this.imageMarkdownAnnouncer = document.getElementById(
'image-markdown-copy-link-announcer',
);
this.imageMarkdownInput = document.getElementById(
'image-markdown-copy-link-input',
);
const isIOSDevice =
navigator.userAgent.match(/iPhone|iPad/i) ||
navigator.userAgent.match('CriOS') ||
navigator.userAgent === 'DEV-Native-ios';
if (isIOSDevice) {
this.imageMarkdownInput.setSelectionRange(
0,
this.imageMarkdownInput.value.length,
);
document.execCommand('copy');
} else {
this.imageMarkdownInput.focus();
this.imageMarkdownInput.setSelectionRange(
0,
this.imageMarkdownInput.value.length,
);
}
this.imageMarkdownAnnouncer.hidden = false;
};
linksToMarkdownForm = imageLinks => {
return imageLinks.map(imageLink => `![Alt Text](${imageLink})`).join('\n');
};
linksToDirectForm = imageLinks => {
return imageLinks.join('\n');
};
render() {
const { onExit, mainImage, version } = this.props;
const { insertionImageUrls, uploadError, uploadErrorMessage } = this.state;
let mainImageArea;
if (mainImage) {
mainImageArea = (
<div>
<img src={mainImage} alt="main" />
<button type="button" onClick={this.triggerMainImageRemoval}>
Remove Cover Image
</button>
</div>
);
} else {
mainImageArea = (
<div>
<input
type="file"
onChange={this.handleMainImageUpload}
accept="image/*"
data-max-file-size-mb="25"
/>
</div>
);
}
let insertionImageArea;
if (insertionImageUrls.length > 0) {
insertionImageArea = (
<div>
<h3>Markdown Images:</h3>
<clipboard-copy
onClick={this.copyText}
for="image-markdown-copy-link-input"
aria-live="polite"
aria-controls="image-markdown-copy-link-announcer"
>
<textarea
id="image-markdown-copy-link-input"
value={this.linksToMarkdownForm(insertionImageUrls)}
/>
<img
id="image-markdown-copy-icon"
src={linkCopyIcon}
alt="Copy to Clipboard"
/>
<span id="image-markdown-copy-link-announcer" role="alert" hidden>
Copied to Clipboard
</span>
</clipboard-copy>
<h3>Direct URLs:</h3>
<textarea
id="image-direct-copy-link-input"
value={this.linksToDirectForm(insertionImageUrls)}
/>
</div>
);
} else {
insertionImageArea = (
<div>
<input
type="file"
onChange={this.handleInsertionImageUpload}
multiple
accept="image/*"
data-max-file-size-mb="25"
/>
</div>
);
}
let imageOptions;
if (version === 'v1') {
imageOptions = (
<div>
<h2>Upload Images</h2>
{insertionImageArea}
<div>
<p>
<em>
To add a cover image for the post, add &nbsp;
<code>cover_image: direct_url_to_image.jpg</code>
&nbsp; to the frontmatter
</em>
</p>
</div>
</div>
);
} else {
imageOptions = (
<div>
<h2>Cover Image</h2>
{mainImageArea}
<h2>Body Images</h2>
{insertionImageArea}
</div>
);
}
return (
<div className="articleform__overlay">
<button
type="button"
className="articleform__exitbutton"
data-content="exit"
onClick={onExit}
>
×
</button>
{uploadError && (
<span className="articleform__uploaderror">{uploadErrorMessage}</span>
)}
{imageOptions}
<div>
<button
type="button"
className="articleform__donebutton"
onClick={onExit}
>
Done
</button>
</div>
</div>
);
}
}
ImageManagement.propTypes = {
onExit: PropTypes.func.isRequired,
onMainImageUrlChange: PropTypes.func.isRequired,
mainImage: PropTypes.string.isRequired,
version: PropTypes.string.isRequired,
};