Skip to content

Commit 451d417

Browse files
bradenmacdonaldpomegranited
authored andcommitted
fix: bugs with ExpandableTextArea toolbars & modals in problem editor (openedx#1646)
* fix: clicking library name in Studio header would show 404 * fix: when ExpandableTextArea is in a modal, the selection toolbar could not be clicked * fix: in ExpandableTextArea, shrink the "insert toolbar" that blocks the input * chore: ignore coverage of modal fixer * fix: make sure emoji/formula modals are working in the text editor too (cherry picked from commit 0b08d82)
1 parent e0ec87c commit 451d417

File tree

3 files changed

+51
-35
lines changed

3 files changed

+51
-35
lines changed

src/editors/sharedComponents/TinyMceWidget/hooks.js

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,40 @@ export const getImageResizeHandler = ({ editor, imagesRef, setImage }) => () =>
148148
});
149149
};
150150

151+
/**
152+
* Fix TinyMCE editors used in Paragon modals, by re-parenting their modal <div>
153+
* from the body to the Paragon modal container.
154+
*
155+
* This fixes a problem where clicking on any modal/popup within TinyMCE (e.g.
156+
* the emoji inserter, the link inserter, the floating format toolbar -
157+
* quickbars, etc.) would cause the parent Paragon modal to close, because
158+
* Paragon sees it as a "click outside" event. Also fixes some hover effects by
159+
* ensuring the layering of the divs is correct.
160+
*
161+
* This could potentially cause problems if there are TinyMCE editors being used
162+
* both on the parent page and inside a Paragon modal popup, but I don't think
163+
* we have that situation.
164+
*
165+
* Note: we can't just do this on init, because the quickbars plugin used by
166+
* ExpandableTextEditors creates its modal DIVs later. Ideally we could listen
167+
* for some kind of "modal open" event, but I haven't been able to find anything
168+
* like that so for now we do this quite frequently, every time there is a
169+
* "selectionchange" event (which is pretty often).
170+
*/
171+
export const reparentTinyMceModals = /* istanbul ignore next */ () => {
172+
const modalLayer = document.querySelector('.pgn__modal-layer');
173+
if (!modalLayer) {
174+
return;
175+
}
176+
const tinymceAuxDivs = document.querySelectorAll('.tox.tox-tinymce-aux');
177+
for (const tinymceAux of tinymceAuxDivs) {
178+
if (tinymceAux.parentElement !== modalLayer) {
179+
// Move this tinyMCE modal div into the paragon modal layer.
180+
modalLayer.appendChild(tinymceAux);
181+
}
182+
}
183+
};
184+
151185
export const setupCustomBehavior = ({
152186
updateContent,
153187
openImgModal,
@@ -221,30 +255,17 @@ export const setupCustomBehavior = ({
221255
}
222256

223257
editor.on('init', /* istanbul ignore next */ () => {
224-
// Moving TinyMce aux modal inside the Editor modal
225-
// if the editor is on modal mode.
226-
// This is to avoid issues using the aux modal:
227-
// * Avoid close aux modal when clicking the content inside.
228-
// * When the user opens the `Edit Source Code` modal, this adds `data-focus-on-hidden`
229-
// to the TinyMce aux modal, making it unusable.
230-
const modalLayer = document.querySelector('.pgn__modal-layer');
231-
const tinymceAux = document.querySelector('.tox.tox-tinymce-aux');
232-
233-
if (modalLayer && tinymceAux) {
234-
modalLayer.appendChild(tinymceAux);
258+
// Check if this editor is inside a (Paragon) modal.
259+
// The way we get the editor's root <div> depends on whether or not this particular editor is using an iframe:
260+
const editorDiv = editor.bodyElement ?? editor.container;
261+
if (editorDiv?.closest('.pgn__modal')) {
262+
// This editor is inside a Paragon modal. Use this hack to avoid interference with TinyMCE's own modal popups:
263+
reparentTinyMceModals();
264+
editor.on('selectionchange', reparentTinyMceModals);
235265
}
236266
});
237267

238268
editor.on('ExecCommand', /* istanbul ignore next */ (e) => {
239-
// Remove `data-focus-on-hidden` and `aria-hidden` on TinyMce aux modal used on emoticons, formulas, etc.
240-
// When using the Editor in modal mode, it may happen that the editor modal is rendered
241-
// before the TinyMce aux modal, which adds these attributes, making the TinyMce aux modal unusable.
242-
const modalElement = document.querySelector('.tox.tox-silver-sink.tox-tinymce-aux');
243-
if (modalElement) {
244-
modalElement.removeAttribute('data-focus-on-hidden');
245-
modalElement.removeAttribute('aria-hidden');
246-
}
247-
248269
if (editorType === 'text' && e.command === 'mceFocus') {
249270
const initialContent = editor.getContent();
250271
const newContent = module.replaceStaticWithAsset({

src/editors/sharedComponents/TinyMceWidget/pluginConfig.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,9 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => {
6464
[editImageSettings],
6565
]),
6666
quickbarsInsertToolbar: toolbar ? false : mapToolbars([
67-
[buttons.undo, buttons.redo],
68-
[buttons.formatSelect],
69-
[buttons.bold, buttons.italic, buttons.underline, buttons.foreColor],
70-
[
71-
buttons.align.justify,
72-
buttons.bullist,
73-
buttons.numlist,
74-
],
75-
[imageUploadButton, buttons.blockQuote, buttons.codeBlock],
76-
[buttons.table, buttons.emoticons, buttons.charmap, buttons.removeFormat, buttons.a11ycheck],
67+
// To keep from blocking the whole text input field when it's empty, this "insert" toolbar
68+
// used with ExpandableTextArea is kept as minimal as we can.
69+
[imageUploadButton, buttons.table],
7770
]),
7871
quickbarsSelectionToolbar: toolbar ? false : mapToolbars([
7972
[buttons.undo, buttons.redo],

src/header/Header.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { getConfig } from '@edx/frontend-platform';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44
import { StudioHeader } from '@edx/frontend-component-header';
55
import { type Container, useToggle } from '@openedx/paragon';
6-
import { generatePath, useHref } from 'react-router-dom';
76

87
import { SearchModal } from '../search-modal';
98
import { useContentMenuItems, useSettingMenuItems, useToolsMenuItems } from './hooks';
@@ -31,7 +30,7 @@ const Header = ({
3130
containerProps = {},
3231
}: HeaderProps) => {
3332
const intl = useIntl();
34-
const libraryHref = useHref('/library/:libraryId');
33+
const waffleFlags = useSelector(getWaffleFlags);
3534

3635
const [isShowSearchModalOpen, openSearchModal, closeSearchModal] = useToggle(false);
3736

@@ -59,9 +58,12 @@ const Header = ({
5958
},
6059
] : [];
6160

62-
const outlineLink = !isLibrary
63-
? `${studioBaseUrl}/course/${contextId}`
64-
: generatePath(libraryHref, { libraryId: contextId });
61+
const getOutlineLink = () => {
62+
if (isLibrary) {
63+
return `/library/${contextId}`;
64+
}
65+
return waffleFlags.useNewCourseOutlinePage ? `/course/${contextId}` : `${studioBaseUrl}/course/${contextId}`;
66+
};
6567

6668
return (
6769
<>

0 commit comments

Comments
 (0)