diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 270295ab1..2abfd4a45 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -5,11 +5,10 @@ import { Collapsible, Icon, IconButton, - Form, + // Form, } from '@edx/paragon'; import { FeedbackOutline, DeleteOutline } from '@edx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - import messages from './messages'; import { selectors } from '../../../../../data/redux'; import { answerOptionProps } from '../../../../../data/services/cms/types'; @@ -17,6 +16,7 @@ import Checker from './components/Checker'; import { FeedbackBox } from './components/Feedback'; import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; +import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; export const AnswerOption = ({ answer, @@ -29,10 +29,10 @@ export const AnswerOption = ({ const dispatch = useDispatch(); const removeAnswer = hooks.removeAnswer({ answer, dispatch }); const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch }); + const setAnswerTitle = hooks.setAnswerTitle({ answer, hasSingleAnswer, dispatch }); const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch }); const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch }); const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer); - return (
`; updatedContent = content.replace(parsedLabels[i - 1], previousLabel); content = updatedContent; - } - if (previousLabel.endsWith('
')) { - updatedLabel = `
${label}`; - updatedContent = content.replace(label, updatedLabel); - content = updatedContent; + updateContent(content); } if (!nextLabel.startsWith('
${nextLabel}`; updatedContent = content.replace(parsedLabels[i + 1], nextLabel); content = updatedContent; + updateContent(content); } } }); + } else { + updateContent(content); } - updateQuestion(content); }; -export const replaceStaticwithAsset = (editor, imageUrls) => { - const content = editor.getContent(); +export const replaceStaticwithAsset = ({ + editor, + imageUrls, + editorType, + lmsEndpointUrl, + updateContent, +}) => { + let content = editor.getContent(); const imageSrcs = content.split('src="'); imageSrcs.forEach(src => { + const currentContent = content; + let staticFullUrl; if (src.startsWith('/static/') && imageUrls.length > 0) { const imgName = src.substring(8, src.indexOf('"')); - let staticFullUrl; imageUrls.forEach((url) => { if (imgName === url.displayName) { staticFullUrl = url.staticFullUrl; + if (editorType === 'expandable') { + staticFullUrl = `${lmsEndpointUrl}${url.staticFullUrl}`; + } } }); if (staticFullUrl) { const currentSrc = src.substring(0, src.indexOf('"')); - const updatedContent = content.replace(currentSrc, staticFullUrl); - editor.setContent(updatedContent); + content = currentContent.replace(currentSrc, staticFullUrl); + if (editorType === 'expandable') { + updateContent(content); + } else { + editor.setContent(content); + } } } }); }; export const setupCustomBehavior = ({ - updateQuestion, + updateContent, openImgModal, openSourceCodeModal, setImage, editorType, imageUrls, + lmsEndpointUrl, }) => (editor) => { // image upload button editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, { @@ -122,17 +136,20 @@ export const setupCustomBehavior = ({ tooltip: 'Apply a "Question" label to specific text, recognized by screen readers. Recommended to improve accessibility.', onAction: toggleLabelFormatting, }); - editor.on('blur', () => { - if (editorType === 'problem') { - module.parseContentForLabels({ + if (editorType === 'expandable') { + editor.on('init', () => { + module.replaceStaticwithAsset({ editor, - updateQuestion, + imageUrls, + editorType, + lmsEndpointUrl, + updateContent, }); - } - }); + }); + } editor.on('ExecCommand', (e) => { - if (e.command === 'mceFocus') { - module.replaceStaticwithAsset(editor, imageUrls); + if (editorType === 'text' && e.command === 'mceFocus') { + module.replaceStaticwithAsset({ editor, imageUrls }); } if (e.command === 'RemoveFormat') { editor.formatter.remove('blockquote'); @@ -157,7 +174,7 @@ export const editorConfig = ({ openImgModal, openSourceCodeModal, setSelection, - updateQuestion, + updateContent, minHeight, }) => { const { @@ -165,6 +182,8 @@ export const editorConfig = ({ config, plugins, imageToolbar, + quickbarsInsertToolbar, + quickbarsSelectionToolbar, } = pluginConfig({ isLibrary, placeholder, editorType }); return { onInit: (evt, editor) => { @@ -187,12 +206,15 @@ export const editorConfig = ({ formats: { label: { inline: 'label' } }, setup: module.setupCustomBehavior({ editorType, - updateQuestion, + updateContent, openImgModal, openSourceCodeModal, + lmsEndpointUrl, setImage: setSelection, imageUrls: module.fetchImageUrls(images), }), + quickbars_insert_toolbar: quickbarsInsertToolbar, + quickbars_selection_toolbar: quickbarsSelectionToolbar, toolbar, plugins, valid_children: '+body[style]', @@ -202,6 +224,16 @@ export const editorConfig = ({ }; }; +export const prepareEditorRef = () => { + const editorRef = useRef(null); + const setEditorRef = useCallback((ref) => { + editorRef.current = ref; + }, []); + const [refReady, setRefReady] = module.state.refReady(false); + useEffect(() => setRefReady(true), []); + return { editorRef, refReady, setEditorRef }; +}; + export const imgModalToggle = () => { const [isImgOpen, setIsOpen] = module.state.isImageModalOpen(false); return { @@ -243,22 +275,27 @@ export const filterAssets = ({ assets }) => { return images; }; -export const setAssetToStaticUrl = ({ editorValue, assets }) => { +export const setAssetToStaticUrl = ({ editorValue, assets, lmsEndpointUrl }) => { /* For assets to remain usable across course instances, we convert their url to be course-agnostic. * For example, /assets/course//filename gets converted to /static/filename. This is * important for rerunning courses and importing/exporting course as the /static/ part of the url * allows the asset to be mapped to the new course run. */ - let content = editorValue; + + // TODO: should probably move this to when the assets are being looped through in the off chance that + // some of the text in the editor contains the lmsEndpointUrl + const regExLmsEndpointUrl = RegExp(lmsEndpointUrl, 'g'); + let content = editorValue.replace(regExLmsEndpointUrl, ''); + const assetUrls = []; const assetsList = Object.values(assets); assetsList.forEach(asset => { assetUrls.push({ portableUrl: asset.portableUrl, displayName: asset.displayName }); }); - const assetSrcs = typeof content === 'string' ? content.split(/(src="|href=")/g) : []; + const assetSrcs = typeof content === 'string' ? content.split(/(src="|src="|href="|href=")/g) : []; assetSrcs.forEach(src => { if (src.startsWith('/asset') && assetUrls.length > 0) { - const assetBlockName = src.substring(src.indexOf('@') + 1, src.indexOf('"')); + const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|")/)); const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1); const nameFromStudioSrc = assetBlockName.substring(assetBlockName.indexOf('/') + 1); let portableUrl; @@ -269,7 +306,7 @@ export const setAssetToStaticUrl = ({ editorValue, assets }) => { } }); if (portableUrl) { - const currentSrc = src.substring(0, src.indexOf('"')); + const currentSrc = src.substring(0, src.search(/("|")/)); const updatedContent = content.replace(currentSrc, portableUrl); content = updatedContent; } diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js index e16cdb5c6..196617bd0 100644 --- a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js +++ b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js @@ -5,6 +5,14 @@ import { keyStore } from '../../utils'; import pluginConfig from './pluginConfig'; import * as module from './hooks'; +jest.mock('react', () => ({ + ...jest.requireActual('react'), + createRef: jest.fn(val => ({ ref: val })), + useRef: jest.fn(val => ({ current: val })), + useEffect: jest.fn(), + useCallback: (cb, prereqs) => ({ cb, prereqs }), +})); + const state = new MockUseState(module); const moduleKeys = keyStore(module); @@ -40,8 +48,9 @@ describe('TinyMceEditor hooks', () => { const openImgModal = jest.fn(); const openSourceCodeModal = jest.fn(); const setImage = jest.fn(); - const updateQuestion = jest.fn(); + const updateContent = jest.fn(); const editorType = 'SOmeEDitor'; + const lmsEndpointUrl = 'sOmEvaLue.cOm'; const editor = { ui: { registry: { addButton, addToggleButton, addIcon } }, on: jest.fn(), @@ -55,10 +64,11 @@ describe('TinyMceEditor hooks', () => { .mockImplementationOnce(mockOpenModalWithImage); output = module.setupCustomBehavior({ editorType, - updateQuestion, + updateContent, openImgModal, openSourceCodeModal, setImage, + lmsEndpointUrl, })(editor); expect(addIcon.mock.calls).toEqual([['textToSpeech', tinyMCE.textToSpeechIcon]]); expect(addButton.mock.calls).toEqual([ @@ -84,30 +94,47 @@ describe('TinyMceEditor hooks', () => { describe('parseContentForLabels', () => { test('it calls getContent and updateQuestion for some content', () => { const editor = { getContent: jest.fn(() => 'Some question labelsome content around a label followed by more text') }; - const updateQuestion = jest.fn(); + const updateContent = jest.fn(); const content = 'Some question labelsome content around a label followed by more text'; - module.parseContentForLabels({ editor, updateQuestion }); + module.parseContentForLabels({ editor, updateContent }); expect(editor.getContent).toHaveBeenCalled(); - expect(updateQuestion).toHaveBeenCalledWith(content); + expect(updateContent).toHaveBeenCalledWith(content); }); test('it calls getContent and updateQuestion for empty content', () => { const editor = { getContent: jest.fn(() => '') }; - const updateQuestion = jest.fn(); + const updateContent = jest.fn(); const content = ''; - module.parseContentForLabels({ editor, updateQuestion }); + module.parseContentForLabels({ editor, updateContent }); expect(editor.getContent).toHaveBeenCalled(); - expect(updateQuestion).toHaveBeenCalledWith(content); + expect(updateContent).toHaveBeenCalledWith(content); }); }); describe('replaceStaticwithAsset', () => { - test('it calls getContent and setContent', () => { + test('it calls getContent and setContent for text editor', () => { const editor = { getContent: jest.fn(() => ''), setContent: jest.fn() }; const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }]; - module.replaceStaticwithAsset(editor, imageUrls); + const lmsEndpointUrl = 'sOmEvaLue.cOm'; + module.replaceStaticwithAsset({ editor, imageUrls, lmsEndpointUrl }); expect(editor.getContent).toHaveBeenCalled(); expect(editor.setContent).toHaveBeenCalled(); }); + test('it calls getContent and updateContent for expandable editor', () => { + const editor = { getContent: jest.fn(() => '') }; + const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }]; + const lmsEndpointUrl = 'sOmEvaLue.cOm'; + const editorType = 'expandable'; + const updateContent = jest.fn(); + module.replaceStaticwithAsset({ + editor, + imageUrls, + editorType, + lmsEndpointUrl, + updateContent, + }); + expect(editor.getContent).toHaveBeenCalled(); + expect(updateContent).toHaveBeenCalled(); + }); }); describe('setAssetToStaticUrl', () => { it('returns content with updated img links', () => { @@ -116,7 +143,8 @@ describe('TinyMceEditor hooks', () => { { portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' }, { portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' }, ]; - const content = module.setAssetToStaticUrl({ editorValue, assets }); + const lmsEndpointUrl = 'sOmEvaLue.cOm'; + const content = module.setAssetToStaticUrl({ editorValue, assets, lmsEndpointUrl }); expect(content).toEqual(' testing link'); }); }); @@ -138,18 +166,22 @@ describe('TinyMceEditor hooks', () => { props.openImgModal = jest.fn(); props.openSourceCodeModal = jest.fn(); props.initializeEditor = jest.fn(); - props.updateQuestion = jest.fn(); + props.updateContent = jest.fn(); jest.spyOn(module, moduleKeys.setupCustomBehavior) .mockImplementationOnce(setupCustomBehavior); output = module.editorConfig(props); }); describe('text editor plugins and toolbar', () => { test('It configures plugins and toolbars correctly', () => { - expect(output.init.plugins).toEqual(pluginConfig({ isLibrary: props.isLibrary }).plugins); - expect(output.init.imagetools_toolbar).toEqual(pluginConfig({ isLibrary: props.isLibrary }).imageToolbar); - expect(output.init.toolbar).toEqual(pluginConfig({ isLibrary: props.isLibrary }).toolbar); - Object.keys(pluginConfig({ isLibrary: props.isLibrary }).config).forEach(key => { - expect(output.init[key]).toEqual(pluginConfig({ isLibrary: props.isLibrary }).config[key]); + const pluginProps = { + isLibrary: props.isLibrary, + editorType: props.editorType, + }; + expect(output.init.plugins).toEqual(pluginConfig(pluginProps).plugins); + expect(output.init.imagetools_toolbar).toEqual(pluginConfig(pluginProps).imageToolbar); + expect(output.init.toolbar).toEqual(pluginConfig(pluginProps).toolbar); + Object.keys(pluginConfig(pluginProps).config).forEach(key => { + expect(output.init[key]).toEqual(pluginConfig(pluginProps).config[key]); }); // Commented out as we investigate whether this is only needed for image proxy // expect(output.init.imagetools_cors_hosts).toMatchObject([props.lmsEndpointUrl]); @@ -159,31 +191,59 @@ describe('TinyMceEditor hooks', () => { test('It configures plugins and toolbars correctly', () => { const pluginProps = { isLibrary: true, + editorType: props.editorType, }; output = module.editorConfig({ ...props, isLibrary: true }); expect(output.init.plugins).toEqual(pluginConfig(pluginProps).plugins); expect(output.init.imagetools_toolbar).toEqual(pluginConfig(pluginProps).imageToolbar); expect(output.init.toolbar).toEqual(pluginConfig(pluginProps).toolbar); + expect(output.init.quickbars_insert_toolbar).toEqual(pluginConfig(pluginProps).quickbarsInsertToolbar); + expect(output.init.quickbars_selection_toolbar).toEqual(pluginConfig(pluginProps).quickbarsSelectionToolbar); Object.keys(pluginConfig(pluginProps).config).forEach(key => { expect(output.init[key]).toEqual(pluginConfig(pluginProps).config[key]); }); }); }); - describe('problem editor plugins and toolbar', () => { + describe('problem editor question plugins and toolbar', () => { test('It configures plugins and toolbars correctly', () => { const pluginProps = { isLibrary: props.isLibrary, - editorType: 'problem', + editorType: 'question', placeholder: 'soMEtExT', }; output = module.editorConfig({ ...props, - editorType: 'problem', + editorType: 'question', placeholder: 'soMEtExT', }); expect(output.init.plugins).toEqual(pluginConfig(pluginProps).plugins); expect(output.init.imagetools_toolbar).toEqual(pluginConfig(pluginProps).imageToolbar); expect(output.init.toolbar).toEqual(pluginConfig(pluginProps).toolbar); + expect(output.init.quickbars_insert_toolbar).toEqual(pluginConfig(pluginProps).quickbarsInsertToolbar); + expect(output.init.quickbars_selection_toolbar).toEqual(pluginConfig(pluginProps).quickbarsSelectionToolbar); + Object.keys(pluginConfig(pluginProps).config).forEach(key => { + expect(output.init[key]).toEqual(pluginConfig(pluginProps).config[key]); + }); + }); + }); + + describe('expandable text area plugins and toolbar', () => { + test('It configures plugins, toolbars, and quick toolbars correctly', () => { + const pluginProps = { + isLibrary: props.isLibrary, + editorType: 'expandable', + placeholder: 'soMEtExT', + }; + output = module.editorConfig({ + ...props, + editorType: 'expandable', + placeholder: 'soMEtExT', + }); + expect(output.init.plugins).toEqual(pluginConfig(pluginProps).plugins); + expect(output.init.imagetools_toolbar).toEqual(pluginConfig(pluginProps).imageToolbar); + expect(output.init.toolbar).toEqual(pluginConfig(pluginProps).toolbar); + expect(output.init.quickbars_insert_toolbar).toEqual(pluginConfig(pluginProps).quickbarsInsertToolbar); + expect(output.init.quickbars_selection_toolbar).toEqual(pluginConfig(pluginProps).quickbarsSelectionToolbar); Object.keys(pluginConfig(pluginProps).config).forEach(key => { expect(output.init[key]).toEqual(pluginConfig(pluginProps).config[key]); }); @@ -207,16 +267,30 @@ describe('TinyMceEditor hooks', () => { expect(output.init.setup).toEqual( setupCustomBehavior({ editorType: props.editorType, - updateQuestion: props.updateQuestion, + updateContent: props.updateContent, openImgModal: props.openImgModal, openSourceCodeModal: props.openSourceCodeModal, setImage: props.setSelection, imageUrls: module.fetchImageUrls(props.images), + lmsEndpointUrl: props.lmsEndpointUrl, }), ); }); }); + describe('filterAssets', () => { + const emptyAssets = {}; + const assets = { sOmEaSsET: { contentType: 'image/' } }; + test('returns an empty array', () => { + const emptyFilterAssets = module.filterAssets({ assets: emptyAssets }); + expect(emptyFilterAssets).toEqual([]); + }); + test('returns filtered array of images', () => { + const FilteredAssets = module.filterAssets({ assets }); + expect(FilteredAssets).toEqual([{ contentType: 'image/' }]); + }); + }); + describe('imgModalToggle', () => { const hookKey = state.keys.isImageModalOpen; beforeEach(() => { diff --git a/src/editors/sharedComponents/TinyMceWidget/index.jsx b/src/editors/sharedComponents/TinyMceWidget/index.jsx index 793df426d..bbaf984d6 100644 --- a/src/editors/sharedComponents/TinyMceWidget/index.jsx +++ b/src/editors/sharedComponents/TinyMceWidget/index.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Editor } from '@tinymce/tinymce-react'; @@ -18,7 +19,9 @@ import 'tinymce/plugins/code'; import 'tinymce/plugins/autoresize'; import 'tinymce/plugins/image'; import 'tinymce/plugins/imagetools'; +import 'tinymce/plugins/quickbars'; +import { selectors } from '../../data/redux'; import ImageUploadModal from '../ImageUploadModal'; import SourceCodeModal from '../SourceCodeModal'; import * as hooks from './hooks'; @@ -26,8 +29,13 @@ import * as hooks from './hooks'; export const TinyMceWidget = ({ editorType, editorRef, + disabled, + id, + // redux assets, isLibrary, + lmsEndpointUrl, + studioEndpointUrl, ...props }) => { const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle(); @@ -42,6 +50,9 @@ export const TinyMceWidget = ({ close={closeImgModal} editorRef={editorRef} images={images} + editorType={editorType} + lmsEndpointUrl={lmsEndpointUrl} + // bookmark={editorRef.current.selection.getBookmark()} {...imageSelection} /> )} @@ -53,6 +64,8 @@ export const TinyMceWidget = ({ /> ) : null} ({ + assets: selectors.app.assets(state), + lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), + studioEndpointUrl: selectors.app.studioEndpointUrl(state), + isLibrary: selectors.app.isLibrary(state), +}); + +export default (connect(mapStateToProps)(TinyMceWidget)); diff --git a/src/editors/sharedComponents/TinyMceWidget/index.test.jsx b/src/editors/sharedComponents/TinyMceWidget/index.test.jsx index cc9cb1ef7..5634b2864 100644 --- a/src/editors/sharedComponents/TinyMceWidget/index.test.jsx +++ b/src/editors/sharedComponents/TinyMceWidget/index.test.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { selectors } from '../../data/redux'; import SourceCodeModal from '../SourceCodeModal'; import ImageUploadModal from '../ImageUploadModal'; import { imgModalToggle, sourceCodeModalToggle } from './hooks'; -import TinyMceEditor from '.'; +import { TinyMceWidget, mapStateToProps } from '.'; // Per https://github.com/tinymce/tinymce-react/issues/91 React unit testing in JSDOM is not supported by tinymce. // Consequently, mock the Editor out. @@ -19,6 +20,17 @@ jest.mock('@tinymce/tinymce-react', () => { jest.mock('../ImageUploadModal', () => 'ImageUploadModal'); jest.mock('../SourceCodeModal', () => 'SourceCodeModal'); +jest.mock('../../data/redux', () => ({ + selectors: { + app: { + lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })), + studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })), + isLibrary: jest.fn(state => ({ isLibrary: state })), + assets: jest.fn(state => ({ assets: state })), + }, + }, +})); + jest.mock('./hooks', () => ({ editorConfig: jest.fn(args => ({ editorConfig: args })), imgModalToggle: jest.fn(() => ({ @@ -39,7 +51,7 @@ jest.mock('./hooks', () => ({ filterAssets: jest.fn(() => [{ staTICUrl: '/assets/sOmEaSsET' }]), })); -describe('TinyMceEditor', () => { +describe('TinyMceWidget', () => { const props = { editorType: 'text', editorRef: { current: { value: 'something' } }, @@ -47,6 +59,8 @@ describe('TinyMceEditor', () => { assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } }, lmsEndpointUrl: 'sOmEvaLue.cOm', studioEndpointUrl: 'sOmEoThERvaLue.cOm', + disabled: false, + id: 'sOMeiD', }; describe('snapshots', () => { imgModalToggle.mockReturnValue({ @@ -60,17 +74,40 @@ describe('TinyMceEditor', () => { closeSourceCodeModal: jest.fn().mockName('modal.closeModal'), }); test('renders as expected with default behavior', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); test('SourcecodeModal is not rendered', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); expect(wrapper.find(SourceCodeModal).length).toBe(0); }); test('ImageUploadModal is not rendered', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); expect(wrapper.find(ImageUploadModal).length).toBe(0); }); }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('lmsEndpointUrl from app.lmsEndpointUrl', () => { + expect( + mapStateToProps(testState).lmsEndpointUrl, + ).toEqual(selectors.app.lmsEndpointUrl(testState)); + }); + test('studioEndpointUrl from app.studioEndpointUrl', () => { + expect( + mapStateToProps(testState).studioEndpointUrl, + ).toEqual(selectors.app.studioEndpointUrl(testState)); + }); + test('assets from app.assets', () => { + expect( + mapStateToProps(testState).assets, + ).toEqual(selectors.app.assets(testState)); + }); + test('isLibrary from app.isLibrary', () => { + expect( + mapStateToProps(testState).isLibrary, + ).toEqual(selectors.app.isLibrary(testState)); + }); + }); }); diff --git a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js index 5384a9fab..a127a9732 100644 --- a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js +++ b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js @@ -8,9 +8,12 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => { const imageTools = isLibrary ? '' : plugins.imagetools; const imageUploadButton = isLibrary ? '' : buttons.imageUploadButton; const editImageSettings = isLibrary ? '' : buttons.editImageSettings; - const codePlugin = editorType === 'problem' ? '' : plugins.code; - const codeButton = editorType === 'problem' ? '' : buttons.code; - const labelButton = editorType === 'problem' ? buttons.customLabelButton : ''; + const codePlugin = editorType === 'text' ? plugins.code : ''; + const codeButton = editorType === 'text' ? buttons.code : ''; + const labelButton = editorType === 'question' ? buttons.customLabelButton : ''; + const quickToolbar = editorType === 'expandable' ? plugins.quickbars : ''; + const inline = editorType === 'expandable'; + const toolbar = editorType !== 'expandable'; return ( StrictDict({ @@ -26,9 +29,10 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => { plugins.autoresize, image, imageTools, + quickToolbar, ].join(' '), menubar: false, - toolbar: mapToolbars([ + toolbar: toolbar ? mapToolbars([ [buttons.undo, buttons.redo], [buttons.formatSelect], [labelButton], @@ -48,21 +52,47 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => { [imageUploadButton, buttons.link, buttons.unlink, buttons.blockQuote, buttons.codeBlock], [buttons.table, buttons.emoticons, buttons.charmap, buttons.hr], [buttons.removeFormat, codeButton], - ]), + ]) : false, imageToolbar: mapToolbars([ // [buttons.rotate.left, buttons.rotate.right], // [buttons.flip.horiz, buttons.flip.vert], [editImageSettings], ]), + quickbarsInsertToolbar: toolbar ? false : mapToolbars([ + [buttons.undo, buttons.redo], + [buttons.formatSelect], + [buttons.bold, buttons.italic, buttons.underline, buttons.foreColor], + [ + buttons.align.justify, + buttons.bullist, + buttons.numlist, + ], + [imageUploadButton, buttons.blockQuote, buttons.codeBlock], + [buttons.table, buttons.emoticons, buttons.charmap, buttons.removeFormat], + ]), + quickbarsSelectionToolbar: toolbar ? false : mapToolbars([ + [buttons.undo, buttons.redo], + [buttons.formatSelect], + [buttons.bold, buttons.italic, buttons.underline, buttons.foreColor], + [ + buttons.align.justify, + buttons.bullist, + buttons.numlist, + ], + [imageUploadButton, buttons.blockQuote, buttons.codeBlock], + [buttons.table, buttons.emoticons, buttons.charmap, buttons.removeFormat], + ]), config: { branding: false, height: '100%', menubar: false, + toolbar_mode: 'sliding', toolbar_sticky: true, toolbar_sticky_offset: 76, relative_urls: true, convert_urls: false, placeholder, + inline, }, }) );
Some question label
some content around a label followed by more text
some content
around a label
followed by more text