terriajs
Version:
Geospatial data visualization platform.
246 lines (224 loc) • 6.25 kB
JSX
import { lazy, Component, Suspense } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Styles from "./story-editor.scss";
import { withTranslation } from "react-i18next";
import tinymce from "tinymce";
import Text from "../../Styled/Text";
import Box from "../../Styled/Box";
import Button from "../../Styled/Button";
// Lazy load the Editor component as the tinyMCE library is large
const Editor = lazy(() => import("../Generic/Editor.tsx"));
class StoryEditor extends Component {
constructor(props) {
super(props);
this.state = {
title: "",
text: "",
id: undefined,
inView: false
};
this.keys = {
ctrl: false,
enter: false
};
this.saveStory = this.saveStory.bind(this);
this.cancelEditing = this.cancelEditing.bind(this);
this.updateTitle = this.updateTitle.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.slideInTimer = null;
this.slideOutTimer = null;
this.escKeyListener = null;
}
UNSAFE_componentWillMount() {
const story = this.props.story;
this.setState({
title: story.title,
text: story.text,
id: story.id
});
}
componentDidMount() {
this.slideIn();
}
slideIn() {
this.slideInTimer = setTimeout(() => {
this.setState({
inView: true
});
this.titleInput.focus();
}, 300);
}
slideOut() {
this.slideOutTimer = this.setState({
inView: false
});
setTimeout(() => {
this.cancelEditing();
}, 300);
}
componentWillUnmount() {
clearTimeout(this.slideInTimer);
if (this.slideOutTimer) {
clearTimeout(this.slideOutTimer);
}
this.setState({
title: "",
text: "",
id: undefined
});
}
updateTitle(event) {
this.setState({
title: event.target.value
});
}
saveStory() {
this.props.saveStory({
title: this.state.title,
text: this.state.text,
id: this.state.id
});
this.setState({
isPopupEditorOpen: false
});
}
cancelEditing() {
this.props.exitEditingMode();
this.setState({
title: this.props.story.title,
text: this.props.story.text
});
}
onKeyDown(event) {
if (event.keyCode === 27) {
this.cancelEditing();
}
if (event.keyCode === 13) {
this.keys.enter = true;
}
if (event.keyCode === 17) {
this.keys.ctrl = true;
}
}
onKeyUp(event) {
if (
(event.keyCode === 13 || event.keyCode === 17) &&
this.keys.enter &&
this.keys.ctrl
) {
this.saveStory();
}
if (event.keyCode === 13) {
this.keys.enter = false;
}
if (event.keyCode === 17) {
this.keys.ctrl = false;
}
}
handleChange(value) {
this.setState({ text: value });
}
removeStory() {
this.props.exitEditingMode();
if (this.state.id) {
this.props.removeStory(this.state.id);
}
}
render() {
const { t } = this.props;
const maxImageHeight = "350px"; // TODO: where to put this to reduce coupling?
return (
<div
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
className={classNames(Styles.popupEditor, {
[Styles.isMounted]: this.state.inView
})}
>
<div className={Styles.inner}>
<div className={Styles.header}>
<Text textLight as="h3" css={{ margin: "0" }}>
{t("story.editor.modalHeader")}
</Text>
</div>
<label htmlFor="title">
<Text small textGreyLighter css={{ marginBottom: "8px" }}>
{t("story.editor.titleLabel")}
</Text>
</label>
<input
ref={(titleInput) => (this.titleInput = titleInput)}
placeholder={t("story.editor.placeholder")}
autoComplete="off"
className={Styles.field}
type="text"
id="title"
value={this.state.title}
onChange={this.updateTitle}
/>
<div className={Styles.body}>
<Text small textGreyLighter css={{ marginBottom: "8px" }}>
{t("story.editor.descriptionLabel")}
</Text>
<Suspense fallback={<div>Loading...</div>}>
<Editor
language={this.props.i18n.language}
html={this.state.text}
onChange={(_newValue, editor) => {
// TODO: This makes StoryEditor tightly coupled to Editor. How to reduce coupling?
tinymce.activeEditor.dom.setStyles(
tinymce.activeEditor.dom.select("img"),
{ "max-height": `${maxImageHeight}`, width: "auto" }
);
const text = editor.getBody().innerHTML;
this.setState({ text });
}}
/>
</Suspense>
</div>
<Box centered gap={3}>
<Button
styledWidth={"240px"}
transparentBg
onClick={this.cancelEditing}
type="button"
title={t("story.editor.cancelBtn")}
textProps={{
textGreyLighter: true,
medium: true
}}
>
{t("story.editor.cancelEditing")}
</Button>
<Button
styledWidth={"240px"}
primary
disabled={!this.state.title.length}
onClick={this.saveStory}
type="button"
title={t("story.editor.saveBtn")}
textProps={{
medium: true
}}
>
{t("story.editor.saveStory")}
</Button>
</Box>
</div>
</div>
);
}
}
StoryEditor.propTypes = {
story: PropTypes.object,
removeStory: PropTypes.func,
saveStory: PropTypes.func,
exitEditingMode: PropTypes.func,
t: PropTypes.func.isRequired,
i18n: PropTypes.object,
terria: PropTypes.object
};
StoryEditor.defaultProps = { story: { title: "", text: "", id: undefined } };
export default withTranslation()(StoryEditor);