Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
React component, in typescript, wrapping Monaco editor to automatically grow & shrink with content to avoid scroll.
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import InlineMonacoEditor from './InlineMonacoEditor';
export const LINES = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Aenean aliquet, nulla eget auctor porttitor, lacus urna',
'posuere purus, at suscipit orci sapien quis est. Curabitur',
'vel erat pulvinar, congue ligula non, convallis urna.',
'Quisque finibus mauris a tempor facilisis. Nam molestie',
'nisl a eros gravida, et eleifend risus interdum. Aliquam',
'faucibus et diam vel lacinia. Aenean bibendum enim nisi,',
'dignissim commodo tortor tincidunt sit amet. Mauris mollis',
'enim at leo volutpat, ac iaculis nunc scelerisque. Aliquam',
'nec accumsan magna. Proin nec justo sed nulla vehicula',
'dictum eu sed nisi. Maecenas commodo odio felis, consequat',
'consequat tellus vestibulum ac. Etiam tempor eu purus at',
'pharetra. Nulla tellus lectus, venenatis id ultricies eget,',
'euismod vel odio. Phasellus in orci in mi placerat interdum',
'vitae vel tellus. Morbi a lectus semper diam condimentum',
'efficitur quis eget leo. Sed hendrerit ante in metus',
'pellentesque, eu iaculis est aliquam. Suspendisse arcu',
'sapien, lobortis egestas euismod at, tincidunt non nibh.'
];
export const actions = {
editorDidMount: action('editorDidMount'),
};
storiesOf('InlineEditor', module)
.add('zero', () => <div>
<hr />
<InlineMonacoEditor {...actions} />
<hr />
</div>)
.add('one', () => <div>
<hr />
<InlineMonacoEditor defaultValue={LINES[0]} {...actions} />
<hr />
</div>)
.add('many', () => <div>
<hr />
<InlineMonacoEditor defaultValue={LINES.join('\n')} {...actions} />
<hr />
</div>)
.add('long', () => <div>
<hr />
<InlineMonacoEditor defaultValue={LINES.join(' ')} {...actions} />
<hr />
</div>);
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react';
import MonacoEditor, { EditorDidMount, MonacoEditorProps } from 'react-monaco-editor';
const LINE_HEIGHT = 18;
const DEFAULT_STATE = {
editor: undefined as unknown as monacoEditor.editor.ICodeEditor,
prevLineCount: -1
}
export default class InlineMonacoEditor extends React.Component<MonacoEditorProps, typeof DEFAULT_STATE> {
constructor(props: MonacoEditorProps) {
super(props);
this.state = DEFAULT_STATE;
}
componentWillUnmount() {
if (window) {
window.removeEventListener('resize', this.setEditorHeight);
}
}
render() {
const { options = {}, editorDidMount } = this.props;
// override a word wrapping, disable and hide the scroll bars
const optionsOverride = {
...options,
wordWrap: 'on',
scrollBeyondLastLine: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
} as monacoEditor.editor.IEditorConstructionOptions;
return (
<MonacoEditor
{...this.props}
editorDidMount={this.editorDidMount(editorDidMount)}
options={optionsOverride} />
);
}
private editorDidMount(prevEditorDidMount: EditorDidMount | undefined): EditorDidMount {
return (editor, monaco) => {
// chain an pre-existing editorDidMount handler
if (prevEditorDidMount) {
prevEditorDidMount(editor, monaco);
}
// put the edit in the state for the handler.
this.setState({ editor });
// do the initial set of the height (wait a bit)
setTimeout(this.setEditorHeight, 0);
// adjust height when the window resizes
if (window) {
window.addEventListener("resize", this.setEditorHeight);
}
// on each edit recompute height (wait a bit)
editor.onDidChangeModelDecorations(() => {
setTimeout(this.setEditorHeight, 0)
});
};
}
setEditorHeight = (() => {
const { editor, prevLineCount } = this.state;
if (!editor) { return; }
const editorDomNode = editor.getDomNode();
if (!editorDomNode) { return; }
const container = editorDomNode.getElementsByClassName('view-lines')[0] as HTMLElement;
const containerHeight = container.offsetHeight;
const lineHeight = container.firstChild
? (container.firstChild as HTMLElement).offsetHeight
: LINE_HEIGHT;
if (!containerHeight) {
// dom hasn't finished settling down. wait a bit more.
setTimeout(this.setEditorHeight, 0);
} else {
const currLineCount = container.childElementCount;
const nextHeight = (prevLineCount > currLineCount)
// if line count is shrinking monaco tends to leave the extra
// space at the end, compute the height from the line count
? currLineCount * lineHeight
// otherwise use the height of the container div as the height
// of the editor node
: containerHeight;
// set the height and redo layout
editorDomNode.style.height = nextHeight + 'px';
editor.layout();
// double check that layout didn't change things too much
if (container.childElementCount !== currLineCount) {
this.setEditorHeight();
} else {
this.setState({ prevLineCount: currLineCount })
}
}
}).bind(this);// bind now so addEventListener/removeEventListener works.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment