* 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
245 lines
6.1 KiB
JavaScript
245 lines
6.1 KiB
JavaScript
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 => ``).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
|
||
<code>cover_image: direct_url_to_image.jpg</code>
|
||
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,
|
||
};
|