terriajs
Version:
Geospatial data visualization platform.
231 lines (196 loc) • 7.39 kB
JSX
import React from "react";
import createReactClass from "create-react-class";
import PropTypes from "prop-types";
import { withTranslation } from "react-i18next";
import { GLYPHS, StyledIcon } from "../../Styled/Icon";
import Styles from "./splitter.scss";
import { observer } from "mobx-react";
import { runInAction } from "mobx";
// Feature detect support for passive: true in event subscriptions.
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
let passiveSupported = false;
try {
const options = Object.defineProperty({}, "passive", {
get: function () {
passiveSupported = true;
return true;
}
});
window.addEventListener("test", null, options);
window.removeEventListener("test", null, options);
} catch (err) {}
const notPassive = passiveSupported ? { passive: false } : false;
const Splitter = observer(
createReactClass({
displayName: "Splitter",
propTypes: {
terria: PropTypes.object.isRequired,
viewState: PropTypes.object.isRequired,
thumbSize: PropTypes.number,
padding: PropTypes.number,
t: PropTypes.func.isRequired
},
getDefaultProps() {
return {
thumbSize: 42,
padding: 0
};
},
componentDidMount() {
const that = this;
window.addEventListener("resize", function () {
that.forceRefresh();
});
},
componentWillUnmount() {
this.unsubscribe();
},
forceRefresh() {
const smallChange =
this.props.terria.splitPosition < 0.5 ? 0.0001 : -0.0001; // Make sure never <0 or >1.
runInAction(() => {
this.props.terria.splitPosition += smallChange;
});
},
startDrag(event) {
const viewer = this.props.terria.currentViewer;
viewer.pauseMapInteraction();
// While dragging is in progress, subscribe to document-level movement and up events.
document.addEventListener("mousemove", this.drag, notPassive);
document.addEventListener("touchmove", this.drag, notPassive);
document.addEventListener("mouseup", this.stopDrag, notPassive);
document.addEventListener("touchend", this.stopDrag, notPassive);
event.preventDefault();
event.stopPropagation();
},
drag(event) {
let clientX = event.clientX;
let clientY = event.clientY;
if (event.targetTouches && event.targetTouches.length > 0) {
clientX = event.targetTouches.item(0).clientX;
clientY = event.targetTouches.item(0).clientY;
}
const viewer = this.props.terria.mainViewer.currentViewer;
const container = viewer.getContainer();
const mapRect = container.getBoundingClientRect();
const that = this;
function computeSplitFraction(startBound, endBound, position) {
const difference = endBound - startBound;
const fraction = (position - startBound) / difference;
const min =
startBound + that.props.padding + that.props.thumbSize * 0.5;
const max = endBound - that.props.padding - that.props.thumbSize * 0.5;
const minFraction = (min - startBound) / difference;
const maxFraction = (max - startBound) / difference;
return Math.min(maxFraction, Math.max(minFraction, fraction));
}
let splitFractionX = computeSplitFraction(
mapRect.left,
mapRect.right,
clientX
);
let splitFractionY = computeSplitFraction(
mapRect.top,
mapRect.bottom,
clientY
);
// We compute the maximum and minium windows bounds as a percentage so that we can always apply the bounds
// restriction as a percentage for consistency (we currently use absolute values for X and percentage values for
// Y, but always apply the constraint as a percentage).
// We use absolute pixel values for horizontal restriction because of the fixed UI elements which occupy an
// absolute amount of screen relestate and 100 px seems like a fine amount for the current UI.
const minX = computeSplitFraction(
mapRect.left,
mapRect.right,
mapRect.left + 100
);
const maxX = computeSplitFraction(
mapRect.left,
mapRect.right,
mapRect.right - 100
);
// Resctrict to within +/-30% of the center vertically (so we don't run into the top and bottom UI elements).
const minY = 0.2;
const maxY = 0.8;
splitFractionX = Math.min(maxX, Math.max(minX, splitFractionX));
splitFractionY = Math.min(maxY, Math.max(minY, splitFractionY));
runInAction(() => {
this.props.terria.splitPosition = splitFractionX;
this.props.terria.splitPositionVertical = splitFractionY;
});
event.preventDefault();
event.stopPropagation();
},
stopDrag(event) {
this.unsubscribe();
const viewer = this.props.terria.currentViewer;
// Ensure splitter stays in sync with map
this.props.viewState.triggerResizeEvent();
viewer.resumeMapInteraction();
event.preventDefault();
event.stopPropagation();
},
unsubscribe() {
document.removeEventListener("mousemove", this.drag, notPassive);
document.removeEventListener("touchmove", this.drag, notPassive);
document.removeEventListener("mouseup", this.stopDrag, notPassive);
document.removeEventListener("touchend", this.stopDrag, notPassive);
window.removeEventListener("resize", this.forceRefresh);
},
getPosition() {
const canvasWidth =
this.props.terria.currentViewer.getContainer().clientWidth;
const canvasHeight =
this.props.terria.currentViewer.getContainer().clientHeight;
return {
x: this.props.terria.splitPosition * canvasWidth,
y: this.props.terria.splitPositionVertical * canvasHeight
};
},
render() {
if (
!this.props.terria.showSplitter ||
!this.props.terria.currentViewer.canShowSplitter ||
!this.props.terria.currentViewer.getContainer()
) {
return null;
}
const thumbWidth = this.props.thumbSize;
const position = this.getPosition();
const dividerStyle = {
left: position.x + "px",
backgroundColor: this.props.terria.baseMapContrastColor
};
const thumbStyle = {
left: position.x + "px",
top: position.y + "px",
width: thumbWidth + "px",
height: thumbWidth + "px",
marginLeft: "-" + thumbWidth * 0.5 + "px",
marginTop: "-" + thumbWidth * 0.5 + "px",
lineHeight: thumbWidth - 2 + "px",
borderRadius: thumbWidth * 0.5 + "px",
fontSize: thumbWidth - 12 + "px"
};
const { t } = this.props;
return (
<div>
<div className={Styles.dividerWrapper}>
<div className={Styles.divider} style={dividerStyle} />
</div>
<button
className={Styles.thumb}
style={thumbStyle}
onClick={(e) => e.preventDefault()}
onMouseDown={this.startDrag}
onTouchStart={this.startDrag}
title={t("splitterTool.title")}
>
<StyledIcon glyph={GLYPHS.splitter} />
</button>
</div>
);
}
})
);
module.exports = withTranslation()(Splitter);