app/ItemView/CodeMirrorContainer.tsx (158 lines of code) (raw):
import React from "react";
// @ts-ignore
import CodeMirror from "codemirror/lib/codemirror"; //see https://github.com/codemirror/CodeMirror/issues/5484
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material-darker.css";
import "codemirror/mode/xml/xml";
import {
createStyles,
StyledComponentProps,
withStyles,
} from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import formatXML from "xml-formatter";
interface CodeMirrorProps {
value: string;
}
interface CodeMirrorContainerState {
failureMessage?: string;
startH: number;
startY: number;
}
const styles = createStyles({
dragHandle: {
background: "#f7f7f7",
height: "20px",
userSelect: "none",
cursor: "row-resize",
borderTop: "1px solid #ddd",
borderBottom: "1px solid #ddd",
"&:before": {
content: "\u2261" /* https://en.wikipedia.org/wiki/Triple_bar */,
color: "#999",
position: "absolute",
left: "50%",
},
"&:hover": {
background: "#f0f0f0",
},
},
});
class CodeMirrorContainer extends React.Component<
CodeMirrorProps & StyledComponentProps,
CodeMirrorContainerState
> {
private textAreaRef = React.createRef<HTMLDivElement>();
private dragHandleRef = React.createRef<HTMLDivElement>();
private CM_MIN_HEIGHT = 200;
//textAreaRef:React.Ref<HTMLTextAreaElement>;
cm?: CodeMirror;
constructor(props: CodeMirrorProps & StyledComponentProps) {
super(props);
this.state = {
startH: -1,
startY: -1,
};
this.onDragHandler = this.onDragHandler.bind(this);
this.onDragRelease = this.onDragRelease.bind(this);
this.startDraggingHandler = this.startDraggingHandler.bind(this);
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("CodeMirrorContainer failed with the error ", error);
console.error(errorInfo);
}
static getDerivedStateFromError(err: Error) {
return {
failureMessage: err.toString,
};
}
/**
* This callback is only used temporarily. It is inserted by `startDraggingHandler` and removed by `onDragRelease`,
* so it is only active while the user has a mouse button down over the drag handle.
* It continuously changes the height of the codemirror instance according to the relative movement of the mouse
* @param evt MouseEvent provided by the DOM
*/
onDragHandler(evt: MouseEvent) {
if (this.cm) {
const calculatedHeight = this.state.startH + evt.y - this.state.startY;
const desiredHeight = Math.max(this.CM_MIN_HEIGHT, calculatedHeight);
this.cm.setSize(null, `${desiredHeight}px`);
} else {
console.error("codeMirror not set in state so can't change it");
}
}
onDragRelease(evt: MouseEvent) {
console.log("drag end");
document.body.removeEventListener("mousemove", this.onDragHandler);
window.removeEventListener("mouseup", this.onDragRelease);
}
getCMHeight() {
if (this.textAreaRef.current) {
const heightString = window
.getComputedStyle(this.textAreaRef.current)
.height.replace(/px$/, "");
return parseInt(heightString);
} else {
console.log("No textAreaRef");
return this.CM_MIN_HEIGHT;
}
}
/**
* This callback is run when the user pushes a mouse button over the drag handle.
* We store the co-ords of the start of the drag in order to compute how much to increase/reduce the size by
* We also register handlers to detect movement within the document, in order to update the size continuously,
* and another to detect mouse-up anywhere in the window to stop the operation.
* @param evt MouseEvent provided by the DOM
*/
startDraggingHandler(evt: MouseEvent) {
if (this.cm) {
console.log("drag start");
this.setState({
startY: evt.y,
startH: this.getCMHeight(),
});
document.body.addEventListener("mousemove", this.onDragHandler);
window.addEventListener("mouseup", this.onDragRelease);
} else {
console.log("CodeMirror not set in state, so can't resize it");
}
}
componentDidMount() {
console.log("textAreaRef is ", this.textAreaRef?.current);
if (this.textAreaRef.current) {
this.cm = CodeMirror(this.textAreaRef.current, {
lineNumbers: true,
mode: "xml",
readOnly: true,
nocursor: true,
});
this.cm.setValue(this.props.value);
}
if (this.dragHandleRef.current) {
console.log("installing startDraggingHandler");
this.dragHandleRef.current.addEventListener(
"mousedown",
this.startDraggingHandler
);
}
}
componentWillUnmount() {
//rely on garbage-collection to free the codemirror object once this object is destroyed
}
componentDidUpdate(
prevProps: Readonly<CodeMirrorProps>,
prevState: Readonly<CodeMirrorContainerState>,
snapshot?: any
) {
if (prevProps.value != this.props.value && this.cm) {
try {
const formatted = formatXML(this.props.value, {
indentation: " ",
lineSeparator: "\n",
});
this.cm.setValue(formatted);
} catch (err) {
console.error("Could not reformat XML: ", err);
this.cm.setValue(this.props.value);
}
}
}
render() {
return this.state.failureMessage ? (
<Alert severity="error">{this.state.failureMessage}</Alert>
) : (
<>
<div ref={this.textAreaRef} />
<div
className={this.props.classes?.dragHandle}
ref={this.dragHandleRef}
/>
</>
);
}
}
export default withStyles(styles)(CodeMirrorContainer);