diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.js b/src/editors/sharedComponents/TinyMceWidget/hooks.js
index d9c5016ced..49ac8a574f 100644
--- a/src/editors/sharedComponents/TinyMceWidget/hooks.js
+++ b/src/editors/sharedComponents/TinyMceWidget/hooks.js
@@ -148,6 +148,40 @@ export const getImageResizeHandler = ({ editor, imagesRef, setImage }) => () =>
});
};
+/**
+ * Fix TinyMCE editors used in Paragon modals, by re-parenting their modal
+ * from the body to the Paragon modal container.
+ *
+ * This fixes a problem where clicking on any modal/popup within TinyMCE (e.g.
+ * the emoji inserter, the link inserter, the floating format toolbar -
+ * quickbars, etc.) would cause the parent Paragon modal to close, because
+ * Paragon sees it as a "click outside" event. Also fixes some hover effects by
+ * ensuring the layering of the divs is correct.
+ *
+ * This could potentially cause problems if there are TinyMCE editors being used
+ * both on the parent page and inside a Paragon modal popup, but I don't think
+ * we have that situation.
+ *
+ * Note: we can't just do this on init, because the quickbars plugin used by
+ * ExpandableTextEditors creates its modal DIVs later. Ideally we could listen
+ * for some kind of "modal open" event, but I haven't been able to find anything
+ * like that so for now we do this quite frequently, every time there is a
+ * "selectionchange" event (which is pretty often).
+ */
+export const reparentTinyMceModals = /* istanbul ignore next */ () => {
+ const modalLayer = document.querySelector('.pgn__modal-layer');
+ if (!modalLayer) {
+ return;
+ }
+ const tinymceAuxDivs = document.querySelectorAll('.tox.tox-tinymce-aux');
+ for (const tinymceAux of tinymceAuxDivs) {
+ if (tinymceAux.parentElement !== modalLayer) {
+ // Move this tinyMCE modal div into the paragon modal layer.
+ modalLayer.appendChild(tinymceAux);
+ }
+ }
+};
+
export const setupCustomBehavior = ({
updateContent,
openImgModal,
@@ -221,30 +255,17 @@ export const setupCustomBehavior = ({
}
editor.on('init', /* istanbul ignore next */ () => {
- // Moving TinyMce aux modal inside the Editor modal
- // if the editor is on modal mode.
- // This is to avoid issues using the aux modal:
- // * Avoid close aux modal when clicking the content inside.
- // * When the user opens the `Edit Source Code` modal, this adds `data-focus-on-hidden`
- // to the TinyMce aux modal, making it unusable.
- const modalLayer = document.querySelector('.pgn__modal-layer');
- const tinymceAux = document.querySelector('.tox.tox-tinymce-aux');
-
- if (modalLayer && tinymceAux) {
- modalLayer.appendChild(tinymceAux);
+ // Check if this editor is inside a (Paragon) modal.
+ // The way we get the editor's root
depends on whether or not this particular editor is using an iframe:
+ const editorDiv = editor.bodyElement ?? editor.container;
+ if (editorDiv?.closest('.pgn__modal')) {
+ // This editor is inside a Paragon modal. Use this hack to avoid interference with TinyMCE's own modal popups:
+ reparentTinyMceModals();
+ editor.on('selectionchange', reparentTinyMceModals);
}
});
editor.on('ExecCommand', /* istanbul ignore next */ (e) => {
- // Remove `data-focus-on-hidden` and `aria-hidden` on TinyMce aux modal used on emoticons, formulas, etc.
- // When using the Editor in modal mode, it may happen that the editor modal is rendered
- // before the TinyMce aux modal, which adds these attributes, making the TinyMce aux modal unusable.
- const modalElement = document.querySelector('.tox.tox-silver-sink.tox-tinymce-aux');
- if (modalElement) {
- modalElement.removeAttribute('data-focus-on-hidden');
- modalElement.removeAttribute('aria-hidden');
- }
-
if (editorType === 'text' && e.command === 'mceFocus') {
const initialContent = editor.getContent();
const newContent = module.replaceStaticWithAsset({
diff --git a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js
index ed64c3e883..720aacc8ba 100644
--- a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js
+++ b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js
@@ -64,16 +64,9 @@ const pluginConfig = ({ placeholder, editorType, enableImageUpload }) => {
[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, buttons.a11ycheck],
+ // To keep from blocking the whole text input field when it's empty, this "insert" toolbar
+ // used with ExpandableTextArea is kept as minimal as we can.
+ [imageUploadButton, buttons.table],
]),
quickbarsSelectionToolbar: toolbar ? false : mapToolbars([
[buttons.undo, buttons.redo],
diff --git a/src/header/Header.tsx b/src/header/Header.tsx
index ebd3199893..d3a7d478d2 100644
--- a/src/header/Header.tsx
+++ b/src/header/Header.tsx
@@ -3,7 +3,6 @@ import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StudioHeader } from '@edx/frontend-component-header';
import { type Container, useToggle } from '@openedx/paragon';
-import { generatePath, useHref } from 'react-router-dom';
import { getWaffleFlags } from '../data/selectors';
import { SearchModal } from '../search-modal';
@@ -32,7 +31,6 @@ const Header = ({
containerProps = {},
}: HeaderProps) => {
const intl = useIntl();
- const libraryHref = useHref('/library/:libraryId');
const waffleFlags = useSelector(getWaffleFlags);
const [isShowSearchModalOpen, openSearchModal, closeSearchModal] = useToggle(false);
@@ -63,7 +61,7 @@ const Header = ({
const getOutlineLink = () => {
if (isLibrary) {
- return generatePath(libraryHref, { libraryId: contextId });
+ return `/library/${contextId}`;
}
return waffleFlags.useNewCourseOutlinePage ? `/course/${contextId}` : `${studioBaseUrl}/course/${contextId}`;
};