Skip to content

Commit

Permalink
Merge pull request #816 from City-of-Helsinki/417-ImageEmbedRichTextE…
Browse files Browse the repository at this point in the history
…ditor

#417-Allow uploading an image and embedding it inside the section HTML in the editor
  • Loading branch information
TeemuVanhamaeki authored Jun 16, 2021
2 parents a085fa0 + 4a7acdf commit 6abd82a
Show file tree
Hide file tree
Showing 19 changed files with 1,038 additions and 559 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ exports[`HearingContainer component should render as expected 1`] = `
"iframeFormFieldWidth": "Leveys pikseleinä (width)",
"iframeHtmlCopyPaste": "Iframe leikkaa ja liitä html",
"iframeModalTitle": "Iframe tiedot",
"imageAddButton": "Lisää kuva",
"imageAddCaptionButton": "Lisää kuvateksti",
"imageModalTitle": "Kuvan tiedot",
"imageSizeError": "Kuva on liian suuri. Maksimikoko on 1 megatavu.",
"inLanguage-en": "englanniksi",
"inLanguage-fi": "suomeksi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ exports[`HearingsList component should render as expected 1`] = `
"iframeFormFieldWidth": "Leveys pikseleinä (width)",
"iframeHtmlCopyPaste": "Iframe leikkaa ja liitä html",
"iframeModalTitle": "Iframe tiedot",
"imageAddButton": "Lisää kuva",
"imageAddCaptionButton": "Lisää kuvateksti",
"imageModalTitle": "Kuvan tiedot",
"imageSizeError": "Kuva on liian suuri. Maksimikoko on 1 megatavu.",
"inLanguage-en": "englanniksi",
"inLanguage-fi": "suomeksi",
Expand Down
3 changes: 3 additions & 0 deletions __tests__/Hearings/__snapshots__/Hearings.react-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ exports[`Hearings component should render as expected 1`] = `
"iframeFormFieldWidth": "Leveys pikseleinä (width)",
"iframeHtmlCopyPaste": "Iframe leikkaa ja liitä html",
"iframeModalTitle": "Iframe tiedot",
"imageAddButton": "Lisää kuva",
"imageAddCaptionButton": "Lisää kuvateksti",
"imageModalTitle": "Kuvan tiedot",
"imageSizeError": "Kuva on liian suuri. Maksimikoko on 1 megatavu.",
"inLanguage-en": "englanniksi",
"inLanguage-fi": "suomeksi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ exports[`SortableCommentList component should render as expected 1`] = `
"iframeFormFieldWidth": "Leveys pikseleinä (width)",
"iframeHtmlCopyPaste": "Iframe leikkaa ja liitä html",
"iframeModalTitle": "Iframe tiedot",
"imageAddButton": "Lisää kuva",
"imageAddCaptionButton": "Lisää kuvateksti",
"imageModalTitle": "Kuvan tiedot",
"imageSizeError": "Kuva on liian suuri. Maksimikoko on 1 megatavu.",
"inLanguage-en": "englanniksi",
"inLanguage-fi": "suomeksi",
Expand Down
4 changes: 4 additions & 0 deletions assets/sass/kerrokantasi/_hearing-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,7 @@
border: 2px solid black;
}
}

.image-modal {
width: 100% !important;
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
"url": "https://github.com/City-of-Helsinki/kerrokantasi-ui/issues"
},
"dependencies": {
"@draft-js-plugins/drag-n-drop": "^4.2.0",
"@draft-js-plugins/editor": "^4.1.0",
"@draft-js-plugins/focus": "^4.1.1",
"@draft-js-plugins/image": "^4.1.1",
"@draft-js-plugins/resizeable": "^4.1.0",
"@octokit/core": ">=3",
"alertifyjs": "^1.13.1",
"autoprefixer": "^8.6.5",
Expand Down Expand Up @@ -93,6 +98,7 @@
"react": "^16.13.1",
"react-anchor-link-smooth-scroll": "^1.0.12",
"react-bootstrap": "0.33.1",
"react-collapse": "^5.1.0",
"react-bootstrap-switch": "^15.5.3",
"react-cookie-consent": "^3.0.0",
"react-datetime": "^2.16.3",
Expand Down
8 changes: 7 additions & 1 deletion src/components/Hearing/Section/SectionContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,13 @@ export class SectionContainerComponent extends React.Component {
if (isEmpty(section.content)) {
return null;
}
return <div dangerouslySetInnerHTML={{ __html: getAttr(section.content, language) }} />;
return <div
dangerouslySetInnerHTML={
{
__html: getAttr(section.content[language].replace(/"\/><\/figure>/gmi, '%"/></figure>'), language)
}
}
/>;
}

renderSectionAbstract = (section, language) => {
Expand Down
15 changes: 14 additions & 1 deletion src/components/RichTextEditor/EditorControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const BLOCK_TYPES = [
{label: 'Väliotsikko', style: 'header-four'},
{label: 'Lista', style: 'unordered-list-item'},
{label: 'Numeroitu lista', style: 'ordered-list-item'},
{label: 'Korostettu kappale', style: 'LEAD'}
{label: 'Korostettu kappale', style: 'LEAD'},
{label: 'Kuvateksti', style: 'ImageCaption'},
];

const INLINE_STYLES = [
Expand Down Expand Up @@ -114,3 +115,15 @@ export const SkipLinkControls = (props) => {
SkipLinkControls.propTypes = {
onClick: PropTypes.func,
};

export const ImageControls = (props) => {
return (
<div className="RichEditor-controls" style={{ display: 'inline-block' }}>
<button className="RichEditor-styleButton" onClick={props.onClick}>{getMessage('imageAddButton')}</button>
</div>
);
};

ImageControls.propTypes = {
onClick: PropTypes.func,
};
16 changes: 16 additions & 0 deletions src/components/RichTextEditor/Image/ImageCaptionEntity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EditorBlock } from 'draft-js';

const ImageCaptionEntity = (props) => {
return (
<EditorBlock {...props} />
);
};

ImageCaptionEntity.propTypes = {
contentState: PropTypes.object,
entityKey: PropTypes.string
};

export default ImageCaptionEntity;
17 changes: 17 additions & 0 deletions src/components/RichTextEditor/Image/ImageEntity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';

const ImageEntity = (props) => {
const {src} = props.contentState.getEntity(props.entityKey).getData();
return (
// eslint-disable-next-line
<img src={src} />
);
};

ImageEntity.propTypes = {
contentState: PropTypes.object,
entityKey: PropTypes.string
};

export default ImageEntity;
139 changes: 139 additions & 0 deletions src/components/RichTextEditor/Image/ImageModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* eslint-disable react/jsx-curly-brace-presence */
import React from 'react';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import getMessage from '../../../utils/getMessage';
import {isFormValid} from '../Iframe/IframeUtils';
import {
ControlLabel,
HelpBlock,
Image,
Modal,
Button,
ModalTitle
} from 'react-bootstrap';
import Dropzone from 'react-dropzone';
import Icon from '../../../utils/Icon';
import {localizedNotifyError} from '../../../utils/notify';

const initialState = {
showFormErrorMsg: false,
fileReaderResult: false
};

/**
* MAX_IMAGE_SIZE given in bytes
*/
const MAX_IMAGE_SIZE = 999999;

class ImageModal extends React.Component {
constructor(props) {
super(props);

this.state = initialState;
this.onFileDrop = this.onFileDrop.bind(this);
this.getImagePreview = this.getImagePreview.bind(this);
this.confirmImage = this.confirmImage.bind(this);
}

validateForm() {
const {fileReaderResult} = this.state;

const inputErrors = {
fileReaderResult: fileReaderResult ? '' : getMessage('validationCantBeEmpty'),
};

return isFormValid(inputErrors);
}


confirmImage() {
const {fileReaderResult} = this.state;

if (this.validateForm()) {
this.props.onSubmit(fileReaderResult);
this.setState(initialState);
} else {
this.setState({
showFormErrorMsg: true,
});
}
}

onFileDrop(files) {
if (files[0].size > MAX_IMAGE_SIZE) {
localizedNotifyError('imageSizeError');
return;
}
const file = files[0]; // Only one file is supported for now.
const fileReader = new FileReader();
fileReader.addEventListener("load", () => {
this.setState({fileReaderResult: fileReader.result});
}, false);
fileReader.readAsDataURL(file);
}

getImagePreview() {
if (this.state.fileReaderResult) {
return (<Image className="preview" src={this.state.fileReaderResult} responsive />);
}
return false;
}

render() {
const { isOpen, onClose } = this.props;
const dropZoneClass = this.state.fileReaderResult ? "dropzone preview" : "dropzone";
return (
<Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<ModalTitle componentClass="h3">
{<FormattedMessage id="imageModalTitle"/>}
</ModalTitle>
</Modal.Header>
<Modal.Body className="form-modal image-modal">
<div className="form-group">
<ControlLabel><FormattedMessage id="sectionImage"/></ControlLabel>
<Dropzone
accept="image/*"
className={dropZoneClass}
multiple={false}
onDrop={this.onFileDrop}
>
{this.getImagePreview()}
<div className="overlay">
<span className="text">
<FormattedMessage id="selectOrDropImage"/>
<Icon className="icon" name="upload"/>
</span>
</div>
</Dropzone>
<HelpBlock><FormattedMessage id="sectionImageHelpText"/></HelpBlock>
</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={onClose}>
<FormattedMessage id="cancel"/>
</Button>
<Button
bsStyle="primary"
onClick={this.confirmImage}
>
{ <FormattedMessage id="formButtonAcceptAndAdd" /> }
</Button>
{this.state.showFormErrorMsg &&
<p id="skip-link-form-submit-error" role="alert" className="rich-text-editor-form-input-error">
{getMessage('formCheckErrors')}
</p>}
</Modal.Footer>
</Modal>
);
}
}

ImageModal.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func,
onSubmit: PropTypes.func,
};

export default ImageModal;
Loading

0 comments on commit 6abd82a

Please sign in to comment.