Skip to content

Commit fec2315

Browse files
refactor(client): render lower jaw via React portal (freeCodeCamp#55785)
1 parent 274b7bc commit fec2315

File tree

1 file changed

+41
-61
lines changed

1 file changed

+41
-61
lines changed

client/src/templates/Challenges/classic/editor.tsx

Lines changed: 41 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import type {
1010
import { OS } from 'monaco-editor/esm/vs/base/common/platform.js';
1111
import Prism from 'prismjs';
1212
import React, { useEffect, Suspense, MutableRefObject, useRef } from 'react';
13-
import ReactDOM from 'react-dom';
14-
import { Provider, connect, useStore } from 'react-redux';
13+
import { createPortal } from 'react-dom';
14+
import { connect } from 'react-redux';
1515
import { createSelector } from 'reselect';
1616
import store from 'store';
1717

@@ -127,7 +127,6 @@ interface EditorProperties {
127127
outputZoneTop: number;
128128
outputZoneId: string;
129129
descriptionNode?: HTMLDivElement;
130-
outputNode?: HTMLDivElement;
131130
descriptionWidget?: editor.IContentWidget;
132131
outputWidget?: editor.IOverlayWidget;
133132
}
@@ -243,7 +242,6 @@ const initialData: EditorProperties = {
243242
};
244243

245244
const Editor = (props: EditorProps): JSX.Element => {
246-
const reduxStore = useStore();
247245
const { t } = useTranslation();
248246
const { editorRef, initTests, resetAttempts, isMobileLayout } = props;
249247
// These refs are used during initialisation of the editor as well as by
@@ -255,6 +253,8 @@ const Editor = (props: EditorProps): JSX.Element => {
255253
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
256254
useRef<typeof monacoEditor>(null);
257255
const dataRef = useRef<EditorProperties>({ ...initialData });
256+
const [lowerJawContainer, setLowerJawContainer] =
257+
React.useState<HTMLDivElement | null>(null);
258258

259259
const submitChallengeDebounceRef = useRef(
260260
debounce(props.submitChallenge, 1000, { leading: true, trailing: false })
@@ -713,56 +713,23 @@ const Editor = (props: EditorProps): JSX.Element => {
713713

714714
const tryToSubmitChallenge = submitChallengeDebounceRef.current;
715715

716-
function createLowerJaw(
717-
outputNode: HTMLDivElement,
718-
editor: editor.IStandaloneCodeEditor
719-
) {
720-
const { output } = props;
721-
const isChallengeComplete = challengeIsComplete();
722-
723-
ReactDOM.render(
724-
<Provider store={reduxStore}>
725-
<LowerJaw
726-
openHelpModal={props.openHelpModal}
727-
openResetModal={props.openResetModal}
728-
tryToExecuteChallenge={tryToExecuteChallenge}
729-
hint={output[1]}
730-
testsLength={props.tests.length}
731-
attempts={attemptsRef.current}
732-
challengeIsCompleted={isChallengeComplete}
733-
tryToSubmitChallenge={tryToSubmitChallenge}
734-
isSignedIn={props.isSignedIn}
735-
updateContainer={() => updateOutputViewZone(outputNode, editor)}
736-
/>
737-
</Provider>,
738-
outputNode
739-
);
740-
}
741-
742-
const updateOutputZone = () => {
743-
const editor = dataRef.current.editor;
744-
if (!editor || !dataRef.current.outputNode) return;
745-
746-
const outputNode = dataRef.current.outputNode;
747-
createLowerJaw(outputNode, editor);
748-
};
749-
750716
// TODO: there's a potential performance gain to be had by only updating when
751717
// the outputViewZone has actually changed.
752718
const updateOutputViewZone = (
753-
outputNode: HTMLDivElement,
754-
editor: editor.IStandaloneCodeEditor
719+
lowerJawContainer: HTMLDivElement,
720+
editor?: editor.IStandaloneCodeEditor
755721
) => {
722+
if (!editor) return;
756723
// make sure the overlayWidget has resized before using it to set the height
757-
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
724+
lowerJawContainer.style.width = `${getEditorContentWidth(editor)}px`;
758725
// We have to wait for the viewZone to finish rendering before adjusting the
759726
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
760727
// not the editor may report the wrong value for position of the lines.
761-
editor?.changeViewZones(changeAccessor => {
728+
editor.changeViewZones(changeAccessor => {
762729
changeAccessor.removeZone(dataRef.current.outputZoneId);
763730
const viewZone = {
764731
afterLineNumber: getLastLineOfEditableRegion(),
765-
heightInPx: outputNode.offsetHeight,
732+
heightInPx: lowerJawContainer.offsetHeight,
766733
domNode: document.createElement('div'),
767734
onComputedHeight: () =>
768735
dataRef.current.outputWidget &&
@@ -829,16 +796,16 @@ const Editor = (props: EditorProps): JSX.Element => {
829796
return editor.getLayoutInfo().contentWidth - getScrollbarWidth();
830797
}
831798

832-
function createOutputNode(editor: editor.IStandaloneCodeEditor) {
833-
if (dataRef.current.outputNode) return dataRef.current.outputNode;
834-
const outputNode = document.createElement('div');
835-
outputNode.classList.add('editor-lower-jaw');
836-
outputNode.setAttribute('id', 'editor-lower-jaw');
837-
outputNode.style.left = `${editor.getLayoutInfo().contentLeft}px`;
838-
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
839-
outputNode.style.top = getOutputZoneTop();
840-
dataRef.current.outputNode = outputNode;
841-
return outputNode;
799+
function createLowerJawContainer(editor: editor.IStandaloneCodeEditor) {
800+
if (lowerJawContainer) return lowerJawContainer;
801+
const container = document.createElement('div');
802+
container.classList.add('editor-lower-jaw');
803+
container.setAttribute('id', 'editor-lower-jaw');
804+
container.style.left = `${editor.getLayoutInfo().contentLeft}px`;
805+
container.style.width = `${getEditorContentWidth(editor)}px`;
806+
container.style.top = getOutputZoneTop();
807+
setLowerJawContainer(container);
808+
return container;
842809
}
843810

844811
function createScrollGutterNode(
@@ -1099,7 +1066,7 @@ const Editor = (props: EditorProps): JSX.Element => {
10991066
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
11001067
const descriptionNode = createDescription(editor);
11011068

1102-
const outputNode = createOutputNode(editor);
1069+
const lowerJawNode = createLowerJawContainer(editor);
11031070

11041071
if (!dataRef.current.descriptionWidget) {
11051072
dataRef.current.descriptionWidget = createWidget(
@@ -1125,11 +1092,10 @@ const Editor = (props: EditorProps): JSX.Element => {
11251092
dataRef.current.outputWidget = createWidget(
11261093
editor,
11271094
'output.widget',
1128-
outputNode,
1095+
lowerJawNode,
11291096
getOutputZoneTop
11301097
);
11311098
editor.addOverlayWidget(dataRef.current.outputWidget);
1132-
editor.changeViewZones(updateOutputZone);
11331099
}
11341100

11351101
editor.onDidScrollChange(() => {
@@ -1157,7 +1123,6 @@ const Editor = (props: EditorProps): JSX.Element => {
11571123
// ask monaco to update regardless.
11581124
redecorateEditableRegion();
11591125
updateDescriptionZone();
1160-
updateOutputZone();
11611126
});
11621127
}
11631128

@@ -1263,7 +1228,6 @@ const Editor = (props: EditorProps): JSX.Element => {
12631228

12641229
useEffect(() => {
12651230
const { model, insideEditDecId } = dataRef.current;
1266-
const lowerJawElement = dataRef.current.outputNode;
12671231
const isChallengeComplete = challengeIsComplete();
12681232
const range = model?.getDecorationRange(insideEditDecId);
12691233
if (range && isChallengeComplete) {
@@ -1275,8 +1239,6 @@ const Editor = (props: EditorProps): JSX.Element => {
12751239
}
12761240
);
12771241
}
1278-
dataRef.current.outputNode = lowerJawElement;
1279-
updateOutputZone();
12801242
// eslint-disable-next-line react-hooks/exhaustive-deps
12811243
}, [props.tests]);
12821244

@@ -1290,7 +1252,6 @@ const Editor = (props: EditorProps): JSX.Element => {
12901252
}
12911253
if (hasEditableRegion()) {
12921254
updateDescriptionZone();
1293-
updateOutputZone();
12941255
}
12951256
// eslint-disable-next-line react-hooks/exhaustive-deps
12961257
}, [props.dimensions]);
@@ -1315,6 +1276,7 @@ const Editor = (props: EditorProps): JSX.Element => {
13151276
: theme === Themes.Default
13161277
? 'vs-custom'
13171278
: editorSystemTheme;
1279+
13181280
return (
13191281
<Suspense fallback={<Loader loaderDelay={600} />}>
13201282
<span className='notranslate'>
@@ -1326,6 +1288,24 @@ const Editor = (props: EditorProps): JSX.Element => {
13261288
theme={editorTheme}
13271289
/>
13281290
</span>
1291+
{lowerJawContainer !== null &&
1292+
createPortal(
1293+
<LowerJaw
1294+
openHelpModal={props.openHelpModal}
1295+
openResetModal={props.openResetModal}
1296+
tryToExecuteChallenge={tryToExecuteChallenge}
1297+
hint={props.output[1]}
1298+
testsLength={props.tests.length}
1299+
attempts={attemptsRef.current}
1300+
challengeIsCompleted={challengeIsComplete()}
1301+
tryToSubmitChallenge={tryToSubmitChallenge}
1302+
isSignedIn={props.isSignedIn}
1303+
updateContainer={() =>
1304+
updateOutputViewZone(lowerJawContainer, dataRef.current.editor)
1305+
}
1306+
/>,
1307+
lowerJawContainer
1308+
)}
13291309
</Suspense>
13301310
);
13311311
};

0 commit comments

Comments
 (0)