Skip to content

Commit

Permalink
fix: bugs with ExpandableTextArea toolbars & modals in problem editor (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
bradenmacdonald authored Feb 12, 2025
1 parent e9130d3 commit 0b08d82
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 33 deletions.
61 changes: 41 additions & 20 deletions src/editors/sharedComponents/TinyMceWidget/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,40 @@ export const getImageResizeHandler = ({ editor, imagesRef, setImage }) => () =>
});
};

/**
* Fix TinyMCE editors used in Paragon modals, by re-parenting their modal <div>
* 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,
Expand Down Expand Up @@ -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 <div> 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({
Expand Down
13 changes: 3 additions & 10 deletions src/editors/sharedComponents/TinyMceWidget/pluginConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
4 changes: 1 addition & 3 deletions src/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}`;
};
Expand Down

0 comments on commit 0b08d82

Please sign in to comment.