feat: insert images into text editor html (#29)
This work adds the functionality that when an image is selected ineither the select image step of the image upload modal, or by using the toolbar inside tinymce, the requisite image is loaded into the settings page, and on the click of the save button, it is inserted into tinymce html.
This commit is contained in:
@@ -28,8 +28,8 @@ export const hooks = {
|
||||
setWidth,
|
||||
};
|
||||
},
|
||||
altText: () => {
|
||||
const [altText, setAltText] = React.useState('');
|
||||
altText: (savedText) => {
|
||||
const [altText, setAltText] = React.useState(savedText || '');
|
||||
const [isDecorative, setIsDecorative] = React.useState(false);
|
||||
return {
|
||||
value: altText,
|
||||
@@ -38,10 +38,10 @@ export const hooks = {
|
||||
setIsDecorative,
|
||||
};
|
||||
},
|
||||
onImgLoad: (initializeDimensions) => ({ target: img }) => {
|
||||
onImgLoad: (initializeDimensions, selection) => ({ target: img }) => {
|
||||
initializeDimensions({
|
||||
height: img.naturalHeight,
|
||||
width: img.naturalWidth,
|
||||
height: selection.height ? selection.height : img.naturalHeight,
|
||||
width: selection.width ? selection.width : img.naturalWidth,
|
||||
});
|
||||
},
|
||||
onInputChange: (handleValue) => (e) => handleValue(e.target.value),
|
||||
@@ -66,9 +66,9 @@ export const ImageSettingsModal = ({
|
||||
returnToSelection,
|
||||
}) => {
|
||||
const dimensions = module.hooks.dimensions();
|
||||
const altText = module.hooks.altText();
|
||||
const onImgLoad = module.hooks.onImgLoad(dimensions.initialize);
|
||||
const onSaveClick = module.hooks.onSave({
|
||||
const altText = module.hooks.altText(selection.altText);
|
||||
const onImgLoad = module.hooks.onImgLoad(dimensions.initialize, selection);
|
||||
const onSaveClick = () => module.hooks.onSave({
|
||||
saveToEditor,
|
||||
dimensions: dimensions.value,
|
||||
altText: altText.value,
|
||||
@@ -94,7 +94,6 @@ export const ImageSettingsModal = ({
|
||||
onLoad={onImgLoad}
|
||||
src={selection.externalUrl}
|
||||
/>
|
||||
|
||||
{ dimensions.value && (
|
||||
<Form.Group>
|
||||
<Form.Label>Image Dimensions</Form.Label>
|
||||
@@ -140,6 +139,7 @@ ImageSettingsModal.propTypes = {
|
||||
selection: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
altText: PropTypes.bool,
|
||||
}).isRequired,
|
||||
saveToEditor: PropTypes.func.isRequired,
|
||||
returnToSelection: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ImageSettingsModal from './ImageSettingsModal';
|
||||
import SelectImageModal from './SelectImageModal';
|
||||
import * as module from './ImageUploadModal';
|
||||
|
||||
export const hooks = {
|
||||
createSaveCallback: ({
|
||||
close, editorRef, setSelection, selection,
|
||||
}) => (settings) => {
|
||||
editorRef.current.execCommand('mceInsertContent', false, module.hooks.getImgTag({ settings, selection }));
|
||||
setSelection(null);
|
||||
close();
|
||||
},
|
||||
getImgTag: ({ settings, selection }) => `<img src="${selection.externalUrl}" alt="${settings.isDecorative ? '' : settings.altText}" width="${settings.dimensions.width}" height="${settings.dimensions.height}">`,
|
||||
};
|
||||
|
||||
const ImageUploadModal = ({
|
||||
// eslint-disable-next-line
|
||||
editorRef,
|
||||
isOpen,
|
||||
close,
|
||||
selection,
|
||||
setSelection,
|
||||
}) => {
|
||||
// selected image file reference data object.
|
||||
// existance of this field determines which child modal is displayed
|
||||
const [selection, setSelection] = React.useState(null);
|
||||
const clearSelection = () => setSelection(null);
|
||||
const saveToEditor = (settings) => {
|
||||
// eslint-disable-next-line
|
||||
console.log({ selection, settings });
|
||||
// tell editor ref to insert content at cursor location();
|
||||
};
|
||||
const saveToEditor = module.hooks.createSaveCallback({
|
||||
close, editorRef, setSelection, selection,
|
||||
});
|
||||
|
||||
const closeAndReset = () => {
|
||||
setSelection(null);
|
||||
close();
|
||||
@@ -31,7 +39,7 @@ const ImageUploadModal = ({
|
||||
close: closeAndReset,
|
||||
selection,
|
||||
saveToEditor,
|
||||
returnToSelection: clearSelection,
|
||||
returnToSelection: () => setSelection(null),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -43,6 +51,12 @@ ImageUploadModal.defaultProps = {
|
||||
editorRef: null,
|
||||
};
|
||||
ImageUploadModal.propTypes = {
|
||||
selection: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
altText: PropTypes.bool,
|
||||
}).isRequired,
|
||||
setSelection: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
editorRef: PropTypes.oneOfType([
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as module from './ImageUploadModal';
|
||||
|
||||
describe('ImageUploadModal hooks', () => {
|
||||
describe('getImgTag', () => {
|
||||
const mockSelection = { externalUrl: 'sOmEuRl.cOm' };
|
||||
let output;
|
||||
test('It returns a html string which matches an image tag', () => {
|
||||
const mockSettings = {
|
||||
altText: 'aLt tExt',
|
||||
isDecorative: false,
|
||||
dimensions: {
|
||||
width: 2022,
|
||||
height: 1619,
|
||||
},
|
||||
|
||||
};
|
||||
output = module.hooks.getImgTag({ selection: mockSelection, settings: mockSettings });
|
||||
expect(output).toEqual(`<img src="${mockSelection.externalUrl}" alt="${mockSettings.altText}" width="${mockSettings.dimensions.width}" height="${mockSettings.dimensions.height}">`);
|
||||
});
|
||||
test('If Is decorative is true, alt text is an empty string', () => {
|
||||
const mockSettings = {
|
||||
isDecorative: true,
|
||||
altText: 'aLt tExt',
|
||||
dimensions: {
|
||||
width: 2022,
|
||||
height: 1619,
|
||||
},
|
||||
};
|
||||
output = module.hooks.getImgTag({ selection: mockSelection, settings: mockSettings });
|
||||
expect(output).toEqual(`<img src="${mockSelection.externalUrl}" alt="" width="${mockSettings.dimensions.width}" height="${mockSettings.dimensions.height}">`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSaveCallback', () => {
|
||||
const close = jest.fn();
|
||||
const execCommandMock = jest.fn();
|
||||
const editorRef = { current: { some: 'dATa', execCommand: execCommandMock } };
|
||||
const setSelection = jest.fn();
|
||||
const selection = jest.fn();
|
||||
const mockSettings = {
|
||||
altText: 'aLt tExt',
|
||||
isDecorative: false,
|
||||
dimensions: {
|
||||
width: 2022,
|
||||
height: 1619,
|
||||
},
|
||||
};
|
||||
let output;
|
||||
beforeEach(() => {
|
||||
output = module.hooks.createSaveCallback({
|
||||
close, editorRef, setSelection, selection,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('It creates a callback, that when called, inserts to the editor, sets the selection to be null, and calls close', () => {
|
||||
jest.spyOn(module.hooks, 'getImgTag').mockImplementationOnce(({ settings }) => ({ selection, settings }));
|
||||
expect(execCommandMock).not.toBeCalled();
|
||||
expect(setSelection).not.toBeCalled();
|
||||
expect(close).not.toBeCalled();
|
||||
output(mockSettings);
|
||||
expect(execCommandMock).toBeCalledWith('mceInsertContent', false, { selection, settings: mockSettings });
|
||||
expect(setSelection).toBeCalledWith(null);
|
||||
expect(close).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,7 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Image } from '@edx/paragon';
|
||||
|
||||
import { actions } from '../../../data/redux';
|
||||
import { thunkActions } from '../../../data/redux';
|
||||
import BaseModal from './BaseModal';
|
||||
import * as module from './SelectImageModal';
|
||||
|
||||
@@ -60,7 +59,7 @@ SelectImageModal.propTypes = {
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
export const mapDispatchToProps = {
|
||||
fetchImages: actions.app.fetchImages,
|
||||
fetchImages: thunkActions.app.fetchImages,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SelectImageModal);
|
||||
|
||||
Reference in New Issue
Block a user