@bigfishtv/cockpit
Version:
311 lines (280 loc) • 8.17 kB
JavaScript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { withFormValue, Field } from '@bigfishtv/react-forms'
import GoogleMap from 'google-map-react'
import _get from 'lodash/get'
import _find from 'lodash/find'
import cn from 'classnames'
import $ from 'jquery'
import Icon from '../Icon'
import Cell from '../cell/Cell'
const defaultMapOptions = maps => ({
panControl: true,
mapTypeControl: false,
scrollwheel: true,
})
const DefaultMarker = props => (
<div style={{ position: 'absolute', left: -18, top: -36, cursor: 'pointer' }}>
<Icon name="place" size={36} />
</div>
)
class GoogleMapsSearchBox extends Component {
static propTypes = {
apiLoaded: PropTypes.bool,
placeholder: PropTypes.string,
onPlaceChanged: PropTypes.func,
}
componentDidMount() {
if (this.props.apiLoaded) this.addListener()
if (window.MutationObserver) {
const observerHack = new MutationObserver(() => {
observerHack.disconnect()
$(this.input).attr('autocomplete', 'new-password')
})
observerHack.observe(this.input, {
attributes: true,
attributeFilter: ['autocomplete'],
})
}
}
componentWillUnmount() {
if (this.props.apiLoaded && this.searchBoxListener) {
google.maps.event.removeListener(this.searchBoxListener)
}
}
componentDidUpdate(prevProps) {
if (prevProps.apiLoaded != this.props.apiLoaded && this.props.apiLoaded) this.addListener()
}
addListener() {
this.autoComplete = new google.maps.places.Autocomplete(this.input, this.props.autoCompleteOptions || {})
this.autoCompleteListener = this.autoComplete.addListener('place_changed', this.onPlaceChanged)
}
onPlaceChanged = () => {
if (this.props.onPlaceChanged) {
this.props.onPlaceChanged(this.autoComplete.getPlace(), this.input.value)
}
}
handleKeyDown = event => {
if (event.key === 'Enter') {
event.preventDefault()
}
}
render() {
const { value, inputProps, error, onInputChange, onInputBlur } = this.props
return (
<div className="form-field" style={{ margin: '0.5rem 0' }}>
<input
{...inputProps}
type="text"
value={value}
className={cn(inputProps.className, error && 'error')}
ref={elem => (this.input = elem)}
onChange={e => onInputChange(e.target.value)}
onKeyDown={this.handleKeyDown}
onBlur={onInputBlur}
/>
</div>
)
}
}
export default class GoogleMapsInput extends Component {
static defaultProps = {
width: '100%',
height: 350,
className: '',
apiKey: null,
selectPrepend: '',
label: 'Location',
instructions: null,
placeholder: 'Search for place...',
showMap: true,
clearInputOnSelect: true,
showTitleField: false,
showAddressField: false,
showDetailFields: false,
isOwnEntity: false,
disableMarkerDrag: true,
inputProps: {},
autoCompleteOptions: {},
createMapOptions: defaultMapOptions,
defaultCenter: [-27.471, 153.0142812],
zoom: 12,
minZoom: 4,
center: null,
Marker: DefaultMarker,
SearchBox: GoogleMapsSearchBox,
}
constructor(props) {
super(props)
this.state = {
inputValue: '',
dragging: false,
apiLoaded: false,
}
}
handlePlaceChange = (place, typedAddress) => {
const { selectPrepend, clearInputOnSelect } = this.props
let address = place.formatted_address
if (place.name && address.indexOf(place.name) < 0) address = place.name + ', ' + address
const newValue = {
[selectPrepend + 'lat']: place.geometry.location.lat(),
[selectPrepend + 'lng']: place.geometry.location.lng(),
[selectPrepend + 'google_maps_url']: place.url,
[selectPrepend + 'address']: address,
}
if (place.name) newValue.title = place.name
this.setState({ inputValue: clearInputOnSelect ? '' : typedAddress }, () => {
this.props.formValue.update({
...(this.props.formValue.value || {}),
...newValue,
})
})
}
handleChildMouseDown = (hoverKey, childProps, mouse) => {
if (this.props.disableMarkerDrag) return
this.setState({
dragging: true,
lat: mouse.lat,
lng: mouse.lng,
latOffset: childProps.lat - mouse.lat,
lngOffset: childProps.lng - mouse.lng,
})
}
handleChildMouseUp = (hoverKey, childProps, mouse) => {
if (this.props.disableMarkerDrag) return
this.setState({ dragging: false })
this.props.formValue.update({
...this.props.formValue.value,
lat: mouse.lat + this.state.latOffset,
lng: mouse.lng + this.state.lngOffset,
})
}
handleChildMouseMove = (hoverKey, childProps, mouse) => {
if (this.props.disableMarkerDrag) return
this.setState({ lat: mouse.lat, lng: mouse.lng })
}
handleChange = mapState => {
if (mapState.zoom) {
this.props.formValue.update({
...this.props.formValue.value,
zoom: mapState.zoom,
})
}
}
handleClear() {
const { selectPrepend, formValue, isOwnEntity } = this.props
if (isOwnEntity) {
formValue.update(null)
} else {
const oldFormValue = formValue.value
const newFormValue = {}
const excludeKeys = ['address', 'lat', 'lng', 'google_maps_url', 'zoom'].map(val => selectPrepend + val)
Object.keys(oldFormValue).forEach(key => {
if (excludeKeys.indexOf(key) < 0) newFormValue[key] = oldFormValue[key]
})
formValue.update(newFormValue)
}
}
render() {
const {
apiKey,
width,
height,
className,
inputProps,
label,
instructions,
selectPrepend,
zoom,
minZoom,
showMap,
placeholder,
createMapOptions,
defaultCenter,
autoCompleteOptions,
formValue,
showTitleField,
showAddressField,
showDetailFields,
Marker,
SearchBox,
} = this.props
const { apiLoaded, dragging, inputValue, latOffset, lngOffset } = this.state
let lat = _get(formValue.value, selectPrepend + 'lat')
let lng = _get(formValue.value, selectPrepend + 'lng')
let address = _get(formValue.value, selectPrepend + 'address')
const center = lat && lng ? [lat, lng] : defaultCenter
const currentZoom = zoom
if (dragging) {
lat = this.state.lat + latOffset
lng = this.state.lng + lngOffset
}
const keyPath = formValue.keyPath.length > 0 ? formValue.keyPath.join('.') + '.' : ''
const addressError = _find(formValue.completeErrorList, {
field: `data.${keyPath}${selectPrepend}address`,
})
return (
<div className={className}>
{showTitleField && <Field select="title" label="Place title" />}
<label>{label}</label>
{instructions && <p className="form-message">{instructions}</p>}
{address ? (
<div>
<Cell
className="cell-input"
icon="place"
title={address}
editable={false}
onRemove={() => this.handleClear()}
/>
{addressError && <div className="form-message error">{addressError.message}</div>}
</div>
) : (
<SearchBox
apiLoaded={apiLoaded}
placeholder={placeholder}
value={inputValue}
inputProps={inputProps}
error={addressError}
autoCompleteOptions={autoCompleteOptions}
onInputChange={inputValue => this.setState({ inputValue })}
onPlaceChanged={this.handlePlaceChange}
/>
)}
<div style={{ width, height: showMap ? height : 0 }}>
<GoogleMap
bootstrapURLKeys={{ key: apiKey, libraries: 'places' }}
onGoogleApiLoaded={() => console.log('API LOADEWD THO') || this.setState({ apiLoaded: true })}
options={createMapOptions}
hoverDistance={20}
center={center}
zoom={currentZoom}
minZoom={minZoom}
draggable={!this.state.dragging}
onChange={this.handleChange}
onChildMouseDown={this.handleChildMouseDown}
onChildMouseUp={this.handleChildMouseUp}
onChildMouseMove={this.handleChildMouseMove}
yesIWantToUseGoogleMapApiInternals>
<Marker lat={center[0]} lng={center[1]} />
</GoogleMap>
</div>
{showAddressField && <Field select="address" />}
{showDetailFields && (
<div className="row gutter-top">
<div className="column-1-3">
<Field select="lat" />
</div>
<div className="column-1-3">
<Field select="lng" />
</div>
<div className="column-1-3">
<Field select="zoom" />
</div>
</div>
)}
</div>
)
}
}