UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

259 lines (225 loc) 7.39 kB
// Copyright (c) 2020 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {Component, createRef} from 'react'; import debounce from 'lodash.debounce'; import isEqual from 'lodash.isequal'; import {canUseDOM} from 'exenv'; import {withTheme} from 'styled-components'; import {RootContext} from 'components/context'; import Modal from 'react-modal'; import window from 'global/window'; import {theme} from 'styles/base'; const listeners = {}; const startListening = () => Object.keys(listeners).forEach(key => listeners[key]()); const getPageOffset = () => ({ x: window.pageXOffset !== undefined ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft, y: window.pageYOffset !== undefined ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop }); const addEventListeners = () => { if (document && document.body) document.body.addEventListener('mousewheel', debounce(startListening, 100, true)); window.addEventListener('resize', debounce(startListening, 50, true)); }; export const getChildPos = ({offsets, rect, childRect, pageOffset, padding}) => { const {topOffset, leftOffset, rightOffset} = offsets; const anchorLeft = leftOffset !== undefined; const pos = { top: pageOffset.y + rect.top + (topOffset || 0), ...(anchorLeft ? {left: pageOffset.x + rect.left + leftOffset} : {right: window.innerWidth - rect.right - pageOffset.x + (rightOffset || 0)}) }; const leftOrRight = anchorLeft ? 'left' : 'right'; if (pos[leftOrRight] && pos[leftOrRight] < 0) { pos[leftOrRight] = padding; } else if (pos[leftOrRight] && pos[leftOrRight] + childRect.width > window.innerWidth) { pos[leftOrRight] = window.innerWidth - childRect.width - padding; } if (pos.top < 0) { pos.top = padding; } else if (pos.top + childRect.height > window.innerHeight) { pos.top = window.innerHeight - childRect.height - padding; } return pos; }; if (canUseDOM) { if (document.body) { addEventListeners(); } else { document.addEventListener('DOMContentLoaded', addEventListeners); } } let listenerIdCounter = 0; function subscribe(fn) { listenerIdCounter += 1; const id = listenerIdCounter; listeners[id] = fn; return () => delete listeners[id]; } const defaultModalStyle = { content: { top: 0, left: 0, border: 0, right: 'auto', bottom: 'auto', padding: '0px 0px 0px 0px' }, overlay: { right: 'auto', bottom: 'auto', width: '100vw', height: '100vh', backgroundColor: 'rgba(0, 0, 0, 0)' } }; const WINDOW_PAD = 40; const noop = () => {}; class Portaled extends Component { static defaultProps = { component: 'div', onClose: noop, theme }; state = { pos: null, isVisible: false }; componentDidMount() { // relative this.unsubscribe = subscribe(this.handleScroll); this.handleScroll(); } componentDidUpdate(prevProps) { const didOpen = this.props.isOpened && !prevProps.isOpened; const didClose = !this.props.isOpened && prevProps.isOpened; if (didOpen || didClose) { window.requestAnimationFrame(() => { if (this._unmounted) return; this.setState({isVisible: didOpen}); }); } this.handleScroll(); } componentWillUnmount() { this._unmounted = true; this.unsubscribe(); } element = createRef(); child = createRef(); // eslint-disable-next-line complexity handleScroll = () => { if (this.child.current) { const rect = this.element.current.getBoundingClientRect(); const childRect = this.child.current && this.child.current.getBoundingClientRect(); const pageOffset = getPageOffset(); const {top: topOffset, left: leftOffset, right: rightOffset} = this.props; const pos = getChildPos({ offsets: {topOffset, leftOffset, rightOffset}, rect, childRect, pageOffset, padding: WINDOW_PAD }); if (!isEqual(pos, this.state.pos)) { this.setState({pos}); } } }; render() { const { // relative component: Comp, overlayZIndex, isOpened, onClose, // Mordal children, modalProps } = this.props; const {isVisible, pos} = this.state; const modalStyle = { ...defaultModalStyle, overlay: { ...defaultModalStyle.overlay, // needs to be on top of existing modal zIndex: overlayZIndex || 9999 } }; return ( <RootContext.Consumer> {context => ( <Comp ref={this.element}> {isOpened ? ( <Modal className="modal-portal" {...modalProps} ariaHideApp={false} isOpen style={modalStyle} parentSelector={() => { // React modal issue: https://github.com/reactjs/react-modal/issues/769 // failed to execute removeChild on parent node when it is already unmounted return ( (context && context.current) || { removeChild: () => {}, appendChild: () => {} } ); }} onRequestClose={onClose} > <div className="portaled-content" key="item" style={{ position: 'fixed', opacity: isVisible ? 1 : 0, top: this.state.top, transition: this.props.theme.transition, marginTop: isVisible ? '0px' : '14px', ...pos }} > <div ref={this.child} style={{ position: 'absolute', zIndex: overlayZIndex ? overlayZIndex + 1 : 10000 }} > {children} </div> </div> </Modal> ) : null} </Comp> )} </RootContext.Consumer> ); } } export default withTheme(Portaled);