higlass
Version:
HiGlass Hi-C / genomic / large data viewer
259 lines (232 loc) • 6.74 kB
JSX
// @ts-nocheck
import { highlight, languages } from 'prismjs/components/prism-core';
import PropTypes from 'prop-types';
import React from 'react';
import Editor from 'react-simple-code-editor';
import 'prismjs/components/prism-json';
import Ajv from 'ajv';
import clsx from 'clsx';
import schema from '../schema.json';
import Button from './Button';
import Dialog from './Dialog';
import withModal from './hocs/with-modal';
import withPubSub from './hocs/with-pub-sub';
import { timeout } from './utils';
import classes from '../styles/ViewConfigEditor.module.scss';
class ViewConfigEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
code: props.viewConfig,
hide: false,
showLog: false,
logMsgs: this.getLogMsgs(props.viewConfig),
};
this.handleChangeBound = this.handleChange.bind(this);
this.handleKeyDownBound = this.handleKeyDown.bind(this);
this.handleKeyUpBound = this.handleKeyUp.bind(this);
this.handleSubmitBound = this.handleSubmit.bind(this);
this.hideBound = this.hide.bind(this);
this.showBound = this.show.bind(this);
this.toggleLogBound = this.toggleLog.bind(this);
this.pubSubs = [];
this.pubSubs.push(
this.props.pubSub.subscribe('keydown', this.handleKeyDownBound),
);
this.pubSubs.push(
this.props.pubSub.subscribe('keyup', this.handleKeyUpBound),
);
}
async componentDidMount() {
if (this.editor) {
this.editor._input.focus();
this.editor._input.setSelectionRange(0, 0);
await timeout(0);
if (this.editorWrap) {
this.editorWrap.scrollTop = 0;
}
}
}
componentWillUnmount() {
this.pubSubs.forEach((subscription) =>
this.props.pubSub.unsubscribe(subscription),
);
this.pubSubs = [];
}
handleChange(code) {
const logMsgs = this.getLogMsgs(code);
this.setState({ code, logMsgs });
}
handleKeyDown(event) {
if (event.key === 's' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
this.props.onChange(this.state.code);
}
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
this.props.onChange(this.state.code);
this.props.modal.close();
}
}
handleKeyUp(event) {
this.setState({ hide: false });
if (event.key === 'Escape') {
event.preventDefault();
this.props.modal.close();
this.props.onCancel();
}
}
handleSubmit(event) {
if (event) event.preventDefault();
this.props.onSave(this.state.code);
}
getLogMsgs(code) {
const logMsgs = [];
let viewConfig;
try {
viewConfig = JSON.parse(code);
} catch (e) {
console.warn(e);
logMsgs.push({ type: 'Error', msg: e.toString() });
return logMsgs;
}
const validate = new Ajv().compile(schema);
const valid = validate(viewConfig);
if (!valid) {
console.warn('Invalid viewconf');
logMsgs.push({ type: 'Warning', msg: 'Invalid viewconf' });
}
if (validate.errors) {
console.warn(JSON.stringify(validate.errors, null, 2));
validate.errors.forEach((e) => {
logMsgs.push({ type: 'Warning', msg: JSON.stringify(e, null, 2) });
});
}
if (logMsgs.length === 0) {
logMsgs.push({ type: 'Success', msg: 'No error or warnings' });
}
return logMsgs;
}
hide() {
this.setState({ hide: true });
}
show() {
this.setState({ hide: false });
}
hideLog() {
this.setState({ showLog: false });
}
showLog() {
this.setState({ showLog: true });
}
toggleLog() {
if (this.state.showLog) {
this.hideLog();
} else {
this.showLog();
}
}
render() {
const logMessages = this.state.logMsgs.map((d, i) => {
const key = `${i}-${d.msg}`;
return (
<tr key={key}>
<td
className={clsx(classes.title, classes[d.type])}
>{`[${i}] ${d.type}`}</td>
<td>
<pre>{d.msg}</pre>
</td>
</tr>
);
});
return (
<Dialog
cancelShortcut="ESC"
cancelTitle="Discard Changes"
hide={this.state.hide}
maxHeight={true}
okayShortcut="⌘+Enter"
okayTitle="Save and Close"
onCancel={this.props.onCancel}
onOkay={this.handleSubmitBound}
title="Edit View Config"
>
<>
<header className={classes['view-config-editor-header']}>
<Button
onBlur={this.showBound}
onMouseDown={this.hideBound}
onMouseOut={this.showBound}
onMouseUp={this.showBound}
>
Hide While Mousedown
</Button>
<Button
onClick={() => {
this.props.onChange(this.state.code);
}}
shortcut="⌘+S"
>
Save
</Button>
</header>
<div
ref={(c) => {
this.editorWrap = c;
}}
className={classes['view-config-editor']}
>
<Editor
ref={(c) => {
this.editor = c;
}}
highlight={(code) => highlight(code, languages.json)}
onValueChange={this.handleChangeBound}
padding={10}
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 'inherit',
}}
value={this.state.code}
/>
</div>
<div
className={classes['view-config-log']}
style={{
height: this.state.showLog ? '50%' : '30px',
}}
>
<div
className={classes['view-config-log-header']}
onClick={() => this.toggleLogBound()}
>
{`Log Messages (${
this.state.logMsgs.filter((d) => d.type !== 'Success').length
})`}
</div>
<div
className={classes['view-config-log-msg']}
style={{
padding: this.state.showLog ? '10px' : 0,
}}
>
<table>
<tbody>{logMessages}</tbody>
</table>
</div>
</div>
</>
</Dialog>
);
}
}
ViewConfigEditor.propTypes = {
modal: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
pubSub: PropTypes.object.isRequired,
viewConfig: PropTypes.string.isRequired,
};
export default withPubSub(withModal(ViewConfigEditor));