refactor(client): render lower jaw via React portal (#55785)
parent
274b7bc377
commit
fec2315447
|
@ -10,8 +10,8 @@ import type {
|
|||
import { OS } from 'monaco-editor/esm/vs/base/common/platform.js';
|
||||
import Prism from 'prismjs';
|
||||
import React, { useEffect, Suspense, MutableRefObject, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider, connect, useStore } from 'react-redux';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import store from 'store';
|
||||
|
||||
|
@ -127,7 +127,6 @@ interface EditorProperties {
|
|||
outputZoneTop: number;
|
||||
outputZoneId: string;
|
||||
descriptionNode?: HTMLDivElement;
|
||||
outputNode?: HTMLDivElement;
|
||||
descriptionWidget?: editor.IContentWidget;
|
||||
outputWidget?: editor.IOverlayWidget;
|
||||
}
|
||||
|
@ -243,7 +242,6 @@ const initialData: EditorProperties = {
|
|||
};
|
||||
|
||||
const Editor = (props: EditorProps): JSX.Element => {
|
||||
const reduxStore = useStore();
|
||||
const { t } = useTranslation();
|
||||
const { editorRef, initTests, resetAttempts, isMobileLayout } = props;
|
||||
// These refs are used during initialisation of the editor as well as by
|
||||
|
@ -255,6 +253,8 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
|
||||
useRef<typeof monacoEditor>(null);
|
||||
const dataRef = useRef<EditorProperties>({ ...initialData });
|
||||
const [lowerJawContainer, setLowerJawContainer] =
|
||||
React.useState<HTMLDivElement | null>(null);
|
||||
|
||||
const submitChallengeDebounceRef = useRef(
|
||||
debounce(props.submitChallenge, 1000, { leading: true, trailing: false })
|
||||
|
@ -713,56 +713,23 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
|
||||
const tryToSubmitChallenge = submitChallengeDebounceRef.current;
|
||||
|
||||
function createLowerJaw(
|
||||
outputNode: HTMLDivElement,
|
||||
editor: editor.IStandaloneCodeEditor
|
||||
) {
|
||||
const { output } = props;
|
||||
const isChallengeComplete = challengeIsComplete();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={reduxStore}>
|
||||
<LowerJaw
|
||||
openHelpModal={props.openHelpModal}
|
||||
openResetModal={props.openResetModal}
|
||||
tryToExecuteChallenge={tryToExecuteChallenge}
|
||||
hint={output[1]}
|
||||
testsLength={props.tests.length}
|
||||
attempts={attemptsRef.current}
|
||||
challengeIsCompleted={isChallengeComplete}
|
||||
tryToSubmitChallenge={tryToSubmitChallenge}
|
||||
isSignedIn={props.isSignedIn}
|
||||
updateContainer={() => updateOutputViewZone(outputNode, editor)}
|
||||
/>
|
||||
</Provider>,
|
||||
outputNode
|
||||
);
|
||||
}
|
||||
|
||||
const updateOutputZone = () => {
|
||||
const editor = dataRef.current.editor;
|
||||
if (!editor || !dataRef.current.outputNode) return;
|
||||
|
||||
const outputNode = dataRef.current.outputNode;
|
||||
createLowerJaw(outputNode, editor);
|
||||
};
|
||||
|
||||
// TODO: there's a potential performance gain to be had by only updating when
|
||||
// the outputViewZone has actually changed.
|
||||
const updateOutputViewZone = (
|
||||
outputNode: HTMLDivElement,
|
||||
editor: editor.IStandaloneCodeEditor
|
||||
lowerJawContainer: HTMLDivElement,
|
||||
editor?: editor.IStandaloneCodeEditor
|
||||
) => {
|
||||
if (!editor) return;
|
||||
// make sure the overlayWidget has resized before using it to set the height
|
||||
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
lowerJawContainer.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
// We have to wait for the viewZone to finish rendering before adjusting the
|
||||
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
||||
// not the editor may report the wrong value for position of the lines.
|
||||
editor?.changeViewZones(changeAccessor => {
|
||||
editor.changeViewZones(changeAccessor => {
|
||||
changeAccessor.removeZone(dataRef.current.outputZoneId);
|
||||
const viewZone = {
|
||||
afterLineNumber: getLastLineOfEditableRegion(),
|
||||
heightInPx: outputNode.offsetHeight,
|
||||
heightInPx: lowerJawContainer.offsetHeight,
|
||||
domNode: document.createElement('div'),
|
||||
onComputedHeight: () =>
|
||||
dataRef.current.outputWidget &&
|
||||
|
@ -829,16 +796,16 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
return editor.getLayoutInfo().contentWidth - getScrollbarWidth();
|
||||
}
|
||||
|
||||
function createOutputNode(editor: editor.IStandaloneCodeEditor) {
|
||||
if (dataRef.current.outputNode) return dataRef.current.outputNode;
|
||||
const outputNode = document.createElement('div');
|
||||
outputNode.classList.add('editor-lower-jaw');
|
||||
outputNode.setAttribute('id', 'editor-lower-jaw');
|
||||
outputNode.style.left = `${editor.getLayoutInfo().contentLeft}px`;
|
||||
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
outputNode.style.top = getOutputZoneTop();
|
||||
dataRef.current.outputNode = outputNode;
|
||||
return outputNode;
|
||||
function createLowerJawContainer(editor: editor.IStandaloneCodeEditor) {
|
||||
if (lowerJawContainer) return lowerJawContainer;
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('editor-lower-jaw');
|
||||
container.setAttribute('id', 'editor-lower-jaw');
|
||||
container.style.left = `${editor.getLayoutInfo().contentLeft}px`;
|
||||
container.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
container.style.top = getOutputZoneTop();
|
||||
setLowerJawContainer(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
function createScrollGutterNode(
|
||||
|
@ -1099,7 +1066,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
|
||||
const descriptionNode = createDescription(editor);
|
||||
|
||||
const outputNode = createOutputNode(editor);
|
||||
const lowerJawNode = createLowerJawContainer(editor);
|
||||
|
||||
if (!dataRef.current.descriptionWidget) {
|
||||
dataRef.current.descriptionWidget = createWidget(
|
||||
|
@ -1125,11 +1092,10 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
dataRef.current.outputWidget = createWidget(
|
||||
editor,
|
||||
'output.widget',
|
||||
outputNode,
|
||||
lowerJawNode,
|
||||
getOutputZoneTop
|
||||
);
|
||||
editor.addOverlayWidget(dataRef.current.outputWidget);
|
||||
editor.changeViewZones(updateOutputZone);
|
||||
}
|
||||
|
||||
editor.onDidScrollChange(() => {
|
||||
|
@ -1157,7 +1123,6 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
// ask monaco to update regardless.
|
||||
redecorateEditableRegion();
|
||||
updateDescriptionZone();
|
||||
updateOutputZone();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1263,7 +1228,6 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
|
||||
useEffect(() => {
|
||||
const { model, insideEditDecId } = dataRef.current;
|
||||
const lowerJawElement = dataRef.current.outputNode;
|
||||
const isChallengeComplete = challengeIsComplete();
|
||||
const range = model?.getDecorationRange(insideEditDecId);
|
||||
if (range && isChallengeComplete) {
|
||||
|
@ -1275,8 +1239,6 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
}
|
||||
);
|
||||
}
|
||||
dataRef.current.outputNode = lowerJawElement;
|
||||
updateOutputZone();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.tests]);
|
||||
|
||||
|
@ -1290,7 +1252,6 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
}
|
||||
if (hasEditableRegion()) {
|
||||
updateDescriptionZone();
|
||||
updateOutputZone();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.dimensions]);
|
||||
|
@ -1315,6 +1276,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
: theme === Themes.Default
|
||||
? 'vs-custom'
|
||||
: editorSystemTheme;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loader loaderDelay={600} />}>
|
||||
<span className='notranslate'>
|
||||
|
@ -1326,6 +1288,24 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||
theme={editorTheme}
|
||||
/>
|
||||
</span>
|
||||
{lowerJawContainer !== null &&
|
||||
createPortal(
|
||||
<LowerJaw
|
||||
openHelpModal={props.openHelpModal}
|
||||
openResetModal={props.openResetModal}
|
||||
tryToExecuteChallenge={tryToExecuteChallenge}
|
||||
hint={props.output[1]}
|
||||
testsLength={props.tests.length}
|
||||
attempts={attemptsRef.current}
|
||||
challengeIsCompleted={challengeIsComplete()}
|
||||
tryToSubmitChallenge={tryToSubmitChallenge}
|
||||
isSignedIn={props.isSignedIn}
|
||||
updateContainer={() =>
|
||||
updateOutputViewZone(lowerJawContainer, dataRef.current.editor)
|
||||
}
|
||||
/>,
|
||||
lowerJawContainer
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue