Feat raw olx editing. TNL-10218 (#182)
* refactor: move CodeEditor to shared components and remove circular dependency * feat: add code editor to problem editor * fix: typo * feat: add save function to raw olx editor and add highlighting * feat: simplify and add tests to edit problem view * feat: add tests to problem edit view * fix: update raw editor tests * fix: code editor tests * fix: package-lock * fix: lint
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditorProblemView component renders raw editor 1`] = `
|
||||
<EditorContainer
|
||||
getContent={[Function]}
|
||||
onClose={null}
|
||||
validateEntry={null}
|
||||
>
|
||||
<Container
|
||||
fluid={true}
|
||||
>
|
||||
<Row>
|
||||
<Component
|
||||
xs={9}
|
||||
>
|
||||
<RawEditor
|
||||
content={null}
|
||||
editorRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
lang="xml"
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
xs={3}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
problemType="advanced"
|
||||
/>
|
||||
</Component>
|
||||
</Row>
|
||||
</Container>
|
||||
</EditorContainer>
|
||||
`;
|
||||
|
||||
exports[`EditorProblemView component renders simple view 1`] = `
|
||||
<EditorContainer
|
||||
getContent={[Function]}
|
||||
onClose={null}
|
||||
validateEntry={null}
|
||||
>
|
||||
<Container
|
||||
fluid={true}
|
||||
>
|
||||
<Row>
|
||||
<Component
|
||||
xs={9}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<AnswerWidget
|
||||
problemType="multiplechoiceresponse"
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
xs={3}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
problemType="multiplechoiceresponse"
|
||||
/>
|
||||
</Component>
|
||||
</Row>
|
||||
</Container>
|
||||
</EditorContainer>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
|
||||
import ReactStateOLXParser from '../../data/ReactStateOLXParser';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const parseState = (problem, isAdvanced, ref) => () => {
|
||||
const reactSettingsParser = new ReactStateSettingsParser(problem);
|
||||
const reactOLXParser = new ReactStateOLXParser({ problem });
|
||||
const rawOLX = ref?.current?.state.doc.toString();
|
||||
|
||||
return {
|
||||
settings: reactSettingsParser.getSettings(),
|
||||
olx: isAdvanced ? rawOLX : reactOLXParser.buildOLX(),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const mockRawOLX = 'rawOLX';
|
||||
const mockBuiltOLX = 'builtOLX';
|
||||
|
||||
jest.mock('../../data/ReactStateOLXParser', () => (
|
||||
jest.fn().mockImplementation(() => ({
|
||||
buildOLX: () => mockBuiltOLX,
|
||||
}))
|
||||
));
|
||||
jest.mock('../../data/ReactStateSettingsParser');
|
||||
|
||||
describe('EditProblemView hooks parseState', () => {
|
||||
const toStringMock = () => mockRawOLX;
|
||||
const refMock = { current: { state: { doc: { toString: toStringMock } } } };
|
||||
|
||||
test('default problem', () => {
|
||||
const res = hooks.parseState('problem', false, refMock)();
|
||||
expect(res.olx).toBe(mockBuiltOLX);
|
||||
});
|
||||
test('advanced problem', () => {
|
||||
const res = hooks.parseState('problem', true, refMock)();
|
||||
expect(res.olx).toBe(mockRawOLX);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
@@ -8,32 +8,33 @@ import SettingsWidget from './SettingsWidget';
|
||||
import QuestionWidget from './QuestionWidget';
|
||||
import { EditorContainer } from '../../../EditorContainer';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
|
||||
import ReactStateOLXParser from '../../data/ReactStateOLXParser';
|
||||
import RawEditor from '../../../../sharedComponents/RawEditor';
|
||||
import { ProblemTypeKeys } from '../../../../data/constants/problem';
|
||||
|
||||
import { parseState } from './hooks';
|
||||
|
||||
export const EditProblemView = ({
|
||||
problemType,
|
||||
problemState,
|
||||
}) => {
|
||||
const parseState = (problem) => () => {
|
||||
const reactSettingsParser = new ReactStateSettingsParser(problem);
|
||||
const reactOLXParser = new ReactStateOLXParser({ problem });
|
||||
return {
|
||||
settings: reactSettingsParser.getSettings(),
|
||||
olx: reactOLXParser.buildOLX(),
|
||||
};
|
||||
};
|
||||
if (problemType === ProblemTypeKeys.ADVANCED) {
|
||||
return 'hello raw editor';
|
||||
}
|
||||
const editorRef = useRef(null);
|
||||
const isAdvancedProblemType = problemType === ProblemTypeKeys.ADVANCED;
|
||||
|
||||
const getContent = parseState(problemState, isAdvancedProblemType, editorRef);
|
||||
|
||||
return (
|
||||
<EditorContainer getContent={parseState(problemState)}>
|
||||
<EditorContainer getContent={getContent}>
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col xs={9}>
|
||||
<QuestionWidget />
|
||||
<AnswerWidget problemType={problemType} />
|
||||
{isAdvancedProblemType ? (
|
||||
<RawEditor editorRef={editorRef} lang="xml" content={problemState.rawOLX} />
|
||||
) : (
|
||||
<>
|
||||
<QuestionWidget />
|
||||
<AnswerWidget problemType={problemType} />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<SettingsWidget problemType={problemType} />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { EditProblemView } from '.';
|
||||
import AnswerWidget from './AnswerWidget';
|
||||
import { ProblemTypeKeys } from '../../../../data/constants/problem';
|
||||
import RawEditor from '../../../../sharedComponents/RawEditor';
|
||||
|
||||
describe('EditorProblemView component', () => {
|
||||
test('renders simple view', () => {
|
||||
const wrapper = shallow(<EditProblemView problemType={ProblemTypeKeys.SINGLESELECT} problemState={{}} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(AnswerWidget).length).toBe(1);
|
||||
});
|
||||
|
||||
test('renders raw editor', () => {
|
||||
const wrapper = shallow(<EditProblemView problemType={ProblemTypeKeys.ADVANCED} problemState={{}} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(AnswerWidget).length).toBe(0);
|
||||
expect(wrapper.find(RawEditor).length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -236,6 +236,13 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
|
||||
/>
|
||||
</Toast>
|
||||
<RawEditor
|
||||
content={
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": "eDiTablE Text",
|
||||
},
|
||||
}
|
||||
}
|
||||
editorRef={
|
||||
Object {
|
||||
"current": Object {
|
||||
@@ -243,13 +250,7 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
|
||||
},
|
||||
}
|
||||
}
|
||||
text={
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": "eDiTablE Text",
|
||||
},
|
||||
}
|
||||
}
|
||||
lang="html"
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import alphanumericMap from './constants';
|
||||
import * as module from './index';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
export const hooks = {
|
||||
|
||||
state: {
|
||||
showBtnEscapeHTML: (val) => React.useState(val),
|
||||
},
|
||||
|
||||
prepareShowBtnEscapeHTML: () => {
|
||||
const [visibility, setVisibility] = hooks.state.showBtnEscapeHTML(true);
|
||||
const hide = () => setVisibility(false);
|
||||
return { showBtnEscapeHTML: visibility, hideBtn: hide };
|
||||
},
|
||||
|
||||
createCodeMirrorDomNode: ({ ref, initialText, upstreamRef }) => {
|
||||
useEffect(() => {
|
||||
const cleanText = hooks.cleanHTML({ initialText });
|
||||
const state = EditorState.create({
|
||||
doc: cleanText,
|
||||
extensions: [basicSetup, html(), EditorView.lineWrapping],
|
||||
});
|
||||
const view = new EditorView({ state, parent: ref.current });
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
upstreamRef.current = view;
|
||||
view.focus();
|
||||
return () => {
|
||||
// called on cleanup
|
||||
view.destroy();
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
cleanHTML: ({ initialText }) => {
|
||||
const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
|
||||
const translator = ($0, $1) => alphanumericMap[$1];
|
||||
return initialText.replace(translateRegex, translator);
|
||||
},
|
||||
escapeHTMLSpecialChars: ({ ref, hideBtn }) => {
|
||||
const text = ref.current.state.doc.toString(); let
|
||||
pos = 0;
|
||||
const changes = [];
|
||||
Object.keys(alphanumericMap).forEach(
|
||||
(escapedKeyword) => {
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
for (let next; (next = text.indexOf(alphanumericMap[escapedKeyword], pos)) > -1;) {
|
||||
changes.push({ from: next, to: next + 1, insert: `&${escapedKeyword};` });
|
||||
pos = next + 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
ref.current.dispatch({ changes });
|
||||
hideBtn();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export const CodeEditor = ({
|
||||
innerRef,
|
||||
value,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const DOMref = useRef();
|
||||
const btnRef = useRef();
|
||||
module.hooks.createCodeMirrorDomNode({ ref: DOMref, initialText: value, upstreamRef: innerRef });
|
||||
const { showBtnEscapeHTML, hideBtn } = module.hooks.prepareShowBtnEscapeHTML();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="CodeMirror" ref={DOMref} />
|
||||
{showBtnEscapeHTML && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label={intl.formatMessage(messages.escapeHTMLButtonLabel)}
|
||||
ref={btnRef}
|
||||
onClick={() => module.hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
|
||||
>
|
||||
<FormattedMessage {...messages.escapeHTMLButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
innerRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.any }),
|
||||
]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CodeEditor);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import CodeEditor from '../CodeEditor';
|
||||
|
||||
export const RawEditor = ({
|
||||
editorRef,
|
||||
text,
|
||||
}) => (
|
||||
<div style={{ padding: '10px 30px', height: '600px' }}>
|
||||
<Alert variant="danger">
|
||||
You are using the raw HTML editor.
|
||||
</Alert>
|
||||
{ text && text.data.data ? (
|
||||
<CodeEditor
|
||||
innerRef={editorRef}
|
||||
value={text.data.data}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
RawEditor.defaultProps = {
|
||||
editorRef: null,
|
||||
text: null,
|
||||
};
|
||||
RawEditor.propTypes = {
|
||||
editorRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.any }),
|
||||
]),
|
||||
text: PropTypes.shape({
|
||||
data: PropTypes.shape({ data: PropTypes.string }),
|
||||
}),
|
||||
};
|
||||
|
||||
export default RawEditor;
|
||||
@@ -12,7 +12,7 @@ import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import BaseModal from '../BaseModal';
|
||||
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import CodeEditor from '../../../../sharedComponents/CodeEditor';
|
||||
|
||||
export const SourceCodeModal = ({
|
||||
isOpen,
|
||||
|
||||
@@ -32,7 +32,7 @@ import { RequestKeys } from '../../data/constants/requests';
|
||||
import EditorContainer from '../EditorContainer';
|
||||
import ImageUploadModal from './components/ImageUploadModal';
|
||||
import SourceCodeModal from './components/SourceCodeModal';
|
||||
import RawEditor from './components/RawEditor';
|
||||
import RawEditor from '../../sharedComponents/RawEditor';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -64,7 +64,7 @@ export const TextEditor = ({
|
||||
return (
|
||||
<RawEditor
|
||||
editorRef={editorRef}
|
||||
text={blockValue}
|
||||
content={blockValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
71
src/editors/sharedComponents/CodeEditor/hooks.js
Normal file
71
src/editors/sharedComponents/CodeEditor/hooks.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
|
||||
import alphanumericMap from './constants';
|
||||
import './index.scss';
|
||||
|
||||
const CODEMIRROR_LANGUAGES = { HTML: 'html', XML: 'xml' };
|
||||
|
||||
export const state = {
|
||||
showBtnEscapeHTML: (val) => React.useState(val),
|
||||
};
|
||||
|
||||
export const prepareShowBtnEscapeHTML = () => {
|
||||
const [visibility, setVisibility] = state.showBtnEscapeHTML(true);
|
||||
const hide = () => setVisibility(false);
|
||||
return { showBtnEscapeHTML: visibility, hideBtn: hide };
|
||||
};
|
||||
|
||||
export const cleanHTML = ({ initialText }) => {
|
||||
const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
|
||||
const translator = ($0, $1) => alphanumericMap[$1];
|
||||
return initialText.replace(translateRegex, translator);
|
||||
};
|
||||
|
||||
export const createCodeMirrorDomNode = ({
|
||||
ref,
|
||||
initialText,
|
||||
upstreamRef,
|
||||
lang,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const languageExtension = lang === CODEMIRROR_LANGUAGES.HTML ? html() : xml();
|
||||
const cleanText = cleanHTML({ initialText });
|
||||
const newState = EditorState.create({
|
||||
doc: cleanText,
|
||||
extensions: [basicSetup, languageExtension, EditorView.lineWrapping],
|
||||
});
|
||||
const view = new EditorView({ state: newState, parent: ref.current });
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
upstreamRef.current = view;
|
||||
view.focus();
|
||||
|
||||
return () => {
|
||||
// called on cleanup
|
||||
view.destroy();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const escapeHTMLSpecialChars = ({ ref, hideBtn }) => {
|
||||
const text = ref.current.state.doc.toString();
|
||||
let pos = 0;
|
||||
const changes = [];
|
||||
Object.keys(alphanumericMap).forEach(
|
||||
(escapedKeyword) => {
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
for (let next; (next = text.indexOf(alphanumericMap[escapedKeyword], pos)) > -1;) {
|
||||
changes.push({ from: next, to: next + 1, insert: `&${escapedKeyword};` });
|
||||
pos = next + 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.current.dispatch({ changes });
|
||||
hideBtn();
|
||||
};
|
||||
55
src/editors/sharedComponents/CodeEditor/index.jsx
Normal file
55
src/editors/sharedComponents/CodeEditor/index.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const CodeEditor = ({
|
||||
innerRef,
|
||||
value,
|
||||
lang,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const DOMref = useRef();
|
||||
const btnRef = useRef();
|
||||
hooks.createCodeMirrorDomNode({
|
||||
ref: DOMref, initialText: value, upstreamRef: innerRef, lang,
|
||||
});
|
||||
const { showBtnEscapeHTML, hideBtn } = hooks.prepareShowBtnEscapeHTML();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="CodeMirror" ref={DOMref} />
|
||||
{showBtnEscapeHTML && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label={intl.formatMessage(messages.escapeHTMLButtonLabel)}
|
||||
ref={btnRef}
|
||||
onClick={() => hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
|
||||
>
|
||||
<FormattedMessage {...messages.escapeHTMLButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
innerRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.any }),
|
||||
]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
lang: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CodeEditor);
|
||||
@@ -4,9 +4,10 @@ import { shallow } from 'enzyme';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { formatMessage, MockUseState } from '../../../../../testUtils';
|
||||
import { formatMessage, MockUseState } from '../../../testUtils';
|
||||
import alphanumericMap from './constants';
|
||||
import * as module from './index';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@codemirror/view');
|
||||
|
||||
@@ -28,11 +29,15 @@ jest.mock('@codemirror/lang-html', () => ({
|
||||
html: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@codemirror/lang-xml', () => ({
|
||||
xml: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('codemirror', () => ({
|
||||
basicSetup: 'bAsiCSetUp',
|
||||
}));
|
||||
|
||||
const state = new MockUseState(module.hooks);
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('CodeEditor', () => {
|
||||
describe('Hooks', () => {
|
||||
@@ -45,7 +50,7 @@ describe('CodeEditor', () => {
|
||||
state.mock();
|
||||
});
|
||||
it('prepareShowBtnEscapeHTML', () => {
|
||||
const hook = module.hooks.prepareShowBtnEscapeHTML();
|
||||
const hook = hooks.prepareShowBtnEscapeHTML();
|
||||
expect(state.stateVals.showBtnEscapeHTML).toEqual(hook.showBtnEscapeHTML);
|
||||
hook.hideBtn();
|
||||
expect(state.setState.showBtnEscapeHTML).toHaveBeenCalledWith(false);
|
||||
@@ -58,7 +63,7 @@ describe('CodeEditor', () => {
|
||||
const cleanText = `${Object.values(alphanumericMap).join(' , ')}`;
|
||||
|
||||
it('escapes alphanumerics and sets them to be literals', () => {
|
||||
expect(module.hooks.cleanHTML({ initialText: dirtyText })).toEqual(cleanText);
|
||||
expect(hooks.cleanHTML({ initialText: dirtyText })).toEqual(cleanText);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +84,7 @@ describe('CodeEditor', () => {
|
||||
};
|
||||
const mockHideBtn = jest.fn();
|
||||
it('unescapes literals and sets them to be alphanumerics', () => {
|
||||
module.hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
|
||||
hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
expect(mockHideBtn).toHaveBeenCalled();
|
||||
});
|
||||
@@ -90,13 +95,14 @@ describe('CodeEditor', () => {
|
||||
ref: {
|
||||
current: 'sOmEvAlUe',
|
||||
},
|
||||
lang: 'html',
|
||||
initialText: 'sOmEhTmL',
|
||||
upstreamRef: {
|
||||
current: 'sOmEotHERvAlUe',
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
module.hooks.createCodeMirrorDomNode(props);
|
||||
hooks.createCodeMirrorDomNode(props);
|
||||
});
|
||||
it('calls useEffect and sets up codemirror objects', () => {
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
@@ -118,18 +124,19 @@ describe('CodeEditor', () => {
|
||||
innerRef: {
|
||||
current: 'sOmEvALUE',
|
||||
},
|
||||
lang: 'html',
|
||||
value: 'mOcKhTmL',
|
||||
};
|
||||
jest.spyOn(module.hooks, 'createCodeMirrorDomNode').mockImplementation(() => ({}));
|
||||
jest.spyOn(hooks, 'createCodeMirrorDomNode').mockImplementation(() => ({}));
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('Renders and calls Hooks ', () => {
|
||||
jest.spyOn(module.hooks, 'prepareShowBtnEscapeHTML').mockImplementation(() => ({ showBtnEscapeHTML: true, hideBtn: mockHideBtn }));
|
||||
jest.spyOn(hooks, 'prepareShowBtnEscapeHTML').mockImplementation(() => ({ showBtnEscapeHTML: true, hideBtn: mockHideBtn }));
|
||||
// Note: ref won't show up as it is not acutaly a DOM attribute.
|
||||
expect(shallow(<module.CodeEditor {...props} />)).toMatchSnapshot();
|
||||
expect(module.hooks.createCodeMirrorDomNode).toHaveBeenCalled();
|
||||
expect(hooks.createCodeMirrorDomNode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,9 @@ exports[`RawEditor renders as expected with default behavior 1`] = `
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
You are using the raw HTML editor.
|
||||
You are using the raw
|
||||
html
|
||||
editor.
|
||||
</Alert>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
innerRef={
|
||||
@@ -22,6 +24,7 @@ exports[`RawEditor renders as expected with default behavior 1`] = `
|
||||
},
|
||||
}
|
||||
}
|
||||
lang="html"
|
||||
value="eDiTablE Text"
|
||||
/>
|
||||
</div>
|
||||
55
src/editors/sharedComponents/RawEditor/index.jsx
Normal file
55
src/editors/sharedComponents/RawEditor/index.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import CodeEditor from '../CodeEditor';
|
||||
|
||||
function getValue(content) {
|
||||
if (!content) { return null; }
|
||||
if (typeof content === 'string') { return content; }
|
||||
return content.data?.data;
|
||||
}
|
||||
|
||||
export const RawEditor = ({
|
||||
editorRef,
|
||||
content,
|
||||
lang,
|
||||
}) => {
|
||||
const value = getValue(content);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px 30px', height: '600px' }}>
|
||||
<Alert variant="danger">
|
||||
You are using the raw {lang} editor.
|
||||
</Alert>
|
||||
{ value ? (
|
||||
<CodeEditor
|
||||
innerRef={editorRef}
|
||||
value={value}
|
||||
lang={lang}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
RawEditor.defaultProps = {
|
||||
editorRef: null,
|
||||
content: null,
|
||||
lang: 'html',
|
||||
};
|
||||
RawEditor.propTypes = {
|
||||
editorRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.any }),
|
||||
]),
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
data: PropTypes.shape({ data: PropTypes.string }),
|
||||
}),
|
||||
]),
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RawEditor;
|
||||
@@ -10,7 +10,7 @@ describe('RawEditor', () => {
|
||||
value: 'Ref Value',
|
||||
},
|
||||
},
|
||||
text: { data: { data: 'eDiTablE Text' } },
|
||||
content: { data: { data: 'eDiTablE Text' } },
|
||||
};
|
||||
test('renders as expected with default behavior', () => {
|
||||
expect(shallow(<RawEditor {...props} />)).toMatchSnapshot();
|
||||
Reference in New Issue
Block a user