mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix:enable default mouse/touch events on monaco content widget (#46217)
* fix:enable default mouse events on monaco content widget * add touch event handling * initial test of scroll gutter * disable scroll gutter * re-enable gutter scroll as content widget * move scroll gutter over line numbers * fix: bug in moving scroll gutter over line numbers * CSS adjustments to put scroll gutter over line numbers * fix: scroll gutter height * fix: make scroll gutter width responsive * refactor: consolidate event handlers * refactor: simplify args to createWidget
This commit is contained in:
@@ -3,6 +3,15 @@
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.monaco-editor-background {
|
||||
overflow: visible !important;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.vs .monaco-scrollable-element > .scrollbar > .slider {
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
@@ -493,6 +493,16 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
if (!getStoredAriaRoledescription()) {
|
||||
setAriaRoledescription(false);
|
||||
}
|
||||
|
||||
// Add invisible content widget over line numbers so touch users will
|
||||
// always have a place to vertically scroll the editor.
|
||||
const scrollGutterNode = createScrollGutterNode(editor);
|
||||
const scrollGutterWidget = createWidget(
|
||||
editor,
|
||||
'scrollgutter.widget',
|
||||
scrollGutterNode
|
||||
);
|
||||
editor.addContentWidget(scrollGutterWidget);
|
||||
};
|
||||
|
||||
const toggleAriaRoledescription = () => {
|
||||
@@ -700,6 +710,19 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
return outputNode;
|
||||
}
|
||||
|
||||
function createScrollGutterNode(
|
||||
editor: editor.IStandaloneCodeEditor
|
||||
): HTMLDivElement {
|
||||
const scrollGutterNode = document.createElement('div');
|
||||
const lineGutterWidth = editor.getLayoutInfo().contentLeft;
|
||||
scrollGutterNode.style.width = `${lineGutterWidth}px`;
|
||||
scrollGutterNode.style.left = `-${lineGutterWidth}px`;
|
||||
scrollGutterNode.style.top = '0';
|
||||
scrollGutterNode.style.height = '10000px';
|
||||
scrollGutterNode.style.background = 'transparent';
|
||||
return scrollGutterNode;
|
||||
}
|
||||
|
||||
function resetMarginDecorations() {
|
||||
const { model, insideEditDecId } = dataRef.current;
|
||||
const range = model?.getDecorationRange(insideEditDecId);
|
||||
@@ -880,42 +903,49 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
})[0];
|
||||
}
|
||||
|
||||
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
|
||||
const createWidget = (
|
||||
id: string,
|
||||
domNode: HTMLDivElement,
|
||||
getTop: () => string
|
||||
) => {
|
||||
const getId = () => id;
|
||||
const getDomNode = () => domNode;
|
||||
const getPosition = () => {
|
||||
const createWidget = (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
id: string,
|
||||
domNode: HTMLDivElement,
|
||||
// If getTop function is not provided then no positioning will be done here.
|
||||
// This allows scroll gutter to do its positioning elsewhere.
|
||||
getTop?: () => string
|
||||
) => {
|
||||
const getId = () => id;
|
||||
const getDomNode = () => domNode;
|
||||
const getPosition = () => {
|
||||
if (getTop) {
|
||||
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
domNode.style.top = getTop();
|
||||
|
||||
// must return null, so that Monaco knows the widget will position
|
||||
// itself.
|
||||
return null;
|
||||
};
|
||||
// Only the description content widget uses this method but it
|
||||
// is harmless to pass it to the overlay widget.
|
||||
const afterRender = () => {
|
||||
domNode.style.left = '0';
|
||||
domNode.style.visibility = 'visible';
|
||||
};
|
||||
return {
|
||||
getId,
|
||||
getDomNode,
|
||||
getPosition,
|
||||
afterRender
|
||||
};
|
||||
}
|
||||
// must return null, so that Monaco knows the widget will position
|
||||
// itself.
|
||||
return null;
|
||||
};
|
||||
// Only the description content widget uses this method but it
|
||||
// is harmless to pass it to the overlay widget.
|
||||
const afterRender = () => {
|
||||
if (getTop) {
|
||||
domNode.style.left = '0';
|
||||
}
|
||||
domNode.style.visibility = 'visible';
|
||||
};
|
||||
return {
|
||||
getId,
|
||||
getDomNode,
|
||||
getPosition,
|
||||
afterRender
|
||||
};
|
||||
};
|
||||
|
||||
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
|
||||
const descriptionNode = createDescription(editor);
|
||||
|
||||
const outputNode = createOutputNode(editor);
|
||||
|
||||
if (!dataRef.current.descriptionWidget) {
|
||||
dataRef.current.descriptionWidget = createWidget(
|
||||
editor,
|
||||
'description.widget',
|
||||
descriptionNode,
|
||||
getDescriptionZoneTop
|
||||
@@ -935,6 +965,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
}
|
||||
if (!dataRef.current.outputWidget) {
|
||||
dataRef.current.outputWidget = createWidget(
|
||||
editor,
|
||||
'output.widget',
|
||||
outputNode,
|
||||
getOutputZoneTop
|
||||
|
||||
@@ -139,6 +139,15 @@ const BASE_LAYOUT = {
|
||||
testsPane: { flex: 0.3 }
|
||||
};
|
||||
|
||||
// Used to prevent monaco from stealing mouse/touch events on the upper jaw
|
||||
// content widget so they can trigger their default actions. (Issue #46166)
|
||||
const handleContentWidgetEvents = (e: MouseEvent | TouchEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target?.closest('.editor-upper-jaw')) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Component
|
||||
class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
static displayName: string;
|
||||
@@ -224,6 +233,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
} = this.props;
|
||||
this.initializeComponent(title);
|
||||
// Bug fix for the monaco content widget and touch devices/right mouse
|
||||
// click. (Issue #46166)
|
||||
document.addEventListener('mousedown', handleContentWidgetEvents, true);
|
||||
document.addEventListener('contextmenu', handleContentWidgetEvents, true);
|
||||
document.addEventListener('touchstart', handleContentWidgetEvents, true);
|
||||
document.addEventListener('touchmove', handleContentWidgetEvents, true);
|
||||
document.addEventListener('touchend', handleContentWidgetEvents, true);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ShowClassicProps) {
|
||||
@@ -301,6 +317,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
const { createFiles, cancelTests } = this.props;
|
||||
createFiles([]);
|
||||
cancelTests();
|
||||
document.removeEventListener('mousedown', handleContentWidgetEvents, true);
|
||||
document.removeEventListener(
|
||||
'contextmenu',
|
||||
handleContentWidgetEvents,
|
||||
true
|
||||
);
|
||||
document.removeEventListener('touchstart', handleContentWidgetEvents, true);
|
||||
document.removeEventListener('touchmove', handleContentWidgetEvents, true);
|
||||
document.removeEventListener('touchend', handleContentWidgetEvents, true);
|
||||
}
|
||||
|
||||
getChallenge = () => this.props.data.challengeNode.challenge;
|
||||
|
||||
Reference in New Issue
Block a user