@@ -100,7 +103,12 @@ class OtpRRExample extends Component {
/** mobile view **/
const mobileView = (
- )} title={(
OpenTripPlanner
)} />
+ }
+ title={
OpenTripPlanner
}
+ />
)
/** the main webapp **/
@@ -108,6 +116,7 @@ class OtpRRExample extends Component {
)
}
diff --git a/lib/actions/api.js b/lib/actions/api.js
index 705822ecb..988e9900e 100644
--- a/lib/actions/api.js
+++ b/lib/actions/api.js
@@ -1,20 +1,22 @@
/* globals fetch */
import { push, replace } from 'connected-react-router'
+import haversine from 'haversine'
+import moment from 'moment'
import hash from 'object-hash'
+import coreUtils from '@opentripplanner/core-utils'
+import queryParams from '@opentripplanner/core-utils/lib/query-params'
import { createAction } from 'redux-actions'
import qs from 'qs'
-import moment from 'moment'
-import haversine from 'haversine'
import { rememberPlace } from './map'
-import { hasCar } from '../util/itinerary'
-import { getTripOptionsFromQuery, getUrlParams } from '../util/query'
-import queryParams from '../util/query-params'
import { getStopViewerConfig, queryIsValid } from '../util/state'
-import { randId } from '../util/storage'
-import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '../util/time'
if (typeof (fetch) === 'undefined') require('isomorphic-fetch')
+const { hasCar } = coreUtils.itinerary
+const { getTripOptionsFromQuery, getUrlParams } = coreUtils.query
+const { randId } = coreUtils.storage
+const { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } = coreUtils.time
+
// Generic API actions
export const nonRealtimeRoutingResponse = createAction('NON_REALTIME_ROUTING_RESPONSE')
diff --git a/lib/actions/form.js b/lib/actions/form.js
index 66860f1a4..abaa4ff00 100644
--- a/lib/actions/form.js
+++ b/lib/actions/form.js
@@ -1,18 +1,10 @@
import debounce from 'lodash.debounce'
+import isEqual from 'lodash.isequal'
import moment from 'moment'
+import coreUtils from '@opentripplanner/core-utils'
import { createAction } from 'redux-actions'
-import isEqual from 'lodash.isequal'
-import {
- getDefaultQuery,
- getTripOptionsFromQuery,
- getUrlParams,
- planParamsToQuery
-} from '../util/query'
-import { getItem, randId } from '../util/storage'
import { queryIsValid } from '../util/state'
-import { OTP_API_TIME_FORMAT } from '../util/time'
-import { isMobile } from '../util/ui'
import {
MobileScreens,
setMainPanelContent,
@@ -21,6 +13,13 @@ import {
import { routingQuery } from './api'
+const {
+ getDefaultQuery,
+ getTripOptionsFromQuery,
+ getUrlParams,
+ planParamsToQuery
+} = coreUtils.query
+
export const settingQueryParam = createAction('SET_QUERY_PARAM')
export const clearActiveSearch = createAction('CLEAR_ACTIVE_SEARCH')
export const setActiveSearch = createAction('SET_ACTIVE_SEARCH')
@@ -35,7 +34,7 @@ export function resetForm () {
dispatch(settingQueryParam(otpState.user.defaults))
} else {
// Get user overrides and apply to default query
- const userOverrides = getItem('defaultQuery', {})
+ const userOverrides = coreUtils.storage.getItem('defaultQuery', {})
const defaultQuery = Object.assign(
getDefaultQuery(otpState.config),
userOverrides
@@ -69,7 +68,7 @@ export function parseUrlQueryString (params = getUrlParams()) {
Object.keys(params).forEach(key => {
if (!key.startsWith('ui_')) planParams[key] = params[key]
})
- const searchId = params.ui_activeSearch || randId()
+ const searchId = params.ui_activeSearch || coreUtils.storage.randId()
// Convert strings to numbers/objects and dispatch
dispatch(
setQueryParam(
@@ -89,10 +88,11 @@ let lastDebouncePlanTimeMs
export function formChanged (oldQuery, newQuery) {
return function (dispatch, getState) {
const otpState = getState().otp
+ const isMobile = coreUtils.ui.isMobile()
// If departArrive is set to 'NOW', update the query time to current
if (otpState.currentQuery && otpState.currentQuery.departArrive === 'NOW') {
- dispatch(settingQueryParam({ time: moment().format(OTP_API_TIME_FORMAT) }))
+ dispatch(settingQueryParam({ time: moment().format(coreUtils.time.OTP_API_TIME_FORMAT) }))
}
// Determine if either from/to location has changed
@@ -111,7 +111,7 @@ export function formChanged (oldQuery, newQuery) {
// either location changes only if not currently on welcome screen (otherwise
// when the current position is auto-set the screen will change unexpectedly).
if (
- isMobile() &&
+ isMobile &&
(fromChanged || toChanged) &&
otpState.ui.mobileScreen !== MobileScreens.WELCOME_SCREEN
) {
@@ -123,8 +123,8 @@ export function formChanged (oldQuery, newQuery) {
const { autoPlan, debouncePlanTimeMs } = otpState.config
const updatePlan =
autoPlan ||
- (!isMobile() && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level?
- (isMobile() && fromChanged && toChanged)
+ (!isMobile && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level?
+ (isMobile && fromChanged && toChanged)
if (updatePlan && queryIsValid(otpState)) { // trip plan should be made
// check if debouncing function needs to be (re)created
if (!debouncedPlanTrip || lastDebouncePlanTimeMs !== debouncePlanTimeMs) {
diff --git a/lib/actions/location.js b/lib/actions/location.js
index 7adf4cbda..e4b778986 100644
--- a/lib/actions/location.js
+++ b/lib/actions/location.js
@@ -18,7 +18,7 @@ export function getCurrentPosition (setAsType = null, onSuccess) {
dispatch(receivedPositionResponse({ position }))
if (setAsType) {
console.log('setting location to current position')
- dispatch(setLocationToCurrent({ type: setAsType }))
+ dispatch(setLocationToCurrent({ locationType: setAsType }))
onSuccess && onSuccess()
}
} else {
diff --git a/lib/actions/map.js b/lib/actions/map.js
index b1b0ece2e..f3ec18447 100644
--- a/lib/actions/map.js
+++ b/lib/actions/map.js
@@ -1,8 +1,9 @@
+import coreUtils from '@opentripplanner/core-utils'
+import getGeocoder from '@opentripplanner/geocoder'
import { createAction } from 'redux-actions'
import { routingQuery } from './api'
import { clearActiveSearch } from './form'
-import getGeocoder from '../util/geocoder'
/* SET_LOCATION action creator. Updates a from or to location in the store
*
@@ -35,6 +36,19 @@ export function clearLocation (payload) {
}
}
+/**
+ * Handler for @opentripplanner/location-field onLocationSelected
+ */
+export function onLocationSelected ({ locationType, location, resultType }) {
+ return function (dispatch, getState) {
+ if (resultType === 'CURRENT_LOCATION') {
+ dispatch(setLocationToCurrent({ locationType }))
+ } else {
+ dispatch(setLocation({ location, locationType }))
+ }
+ }
+}
+
export function setLocation (payload) {
return function (dispatch, getState) {
const otpState = getState().otp
@@ -45,12 +59,12 @@ export function setLocation (payload) {
.reverse({ point: payload.location })
.then((location) => {
dispatch(settingLocation({
- type: payload.type,
+ locationType: payload.locationType,
location
}))
}).catch(err => {
dispatch(settingLocation({
- type: payload.type,
+ locationType: payload.locationType,
location: payload.location
}))
console.warn(err)
@@ -83,11 +97,11 @@ export function switchLocations () {
const { from, to } = getState().otp.currentQuery
// First, reverse the locations.
dispatch(settingLocation({
- type: 'from',
+ locationType: 'from',
location: to
}))
dispatch(settingLocation({
- type: 'to',
+ locationType: 'to',
location: from
}))
// Then kick off a routing query (if the query is invalid, search will abort).
@@ -101,11 +115,12 @@ export const setElevationPoint = createAction('SET_ELEVATION_POINT')
export const setMapPopupLocation = createAction('SET_MAP_POPUP_LOCATION')
-export function setMapPopupLocationAndGeocode (payload) {
+export function setMapPopupLocationAndGeocode (mapEvent) {
+ const location = coreUtils.map.constructLocation(mapEvent.latlng)
return function (dispatch, getState) {
- dispatch(setMapPopupLocation(payload))
+ dispatch(setMapPopupLocation({ location }))
getGeocoder(getState().otp.config.geocoder)
- .reverse({ point: payload.location })
+ .reverse({ point: location })
.then((location) => {
dispatch(setMapPopupLocation({ location }))
}).catch(err => {
diff --git a/lib/actions/narrative.js b/lib/actions/narrative.js
index 8f016a400..56cc5279c 100644
--- a/lib/actions/narrative.js
+++ b/lib/actions/narrative.js
@@ -1,14 +1,14 @@
+import coreUtils from '@opentripplanner/core-utils'
import { createAction } from 'redux-actions'
import { setUrlSearch } from './api'
-import { getUrlParams } from '../util/query'
export function setActiveItinerary (payload) {
return function (dispatch, getState) {
// Trigger change in store.
dispatch(settingActiveitinerary(payload))
// Update URL params.
- const urlParams = getUrlParams()
+ const urlParams = coreUtils.query.getUrlParams()
urlParams.ui_activeItinerary = payload.index
dispatch(setUrlSearch(urlParams))
}
diff --git a/lib/actions/ui.js b/lib/actions/ui.js
index ad04d2805..9dc8decfe 100644
--- a/lib/actions/ui.js
+++ b/lib/actions/ui.js
@@ -1,13 +1,14 @@
+import { push } from 'connected-react-router'
+import coreUtils from '@opentripplanner/core-utils'
import { createAction } from 'redux-actions'
import { matchPath } from 'react-router'
-import { push } from 'connected-react-router'
import { findRoute } from './api'
import { setMapCenter, setMapZoom, setRouterId } from './config'
import { clearActiveSearch, parseUrlQueryString, setActiveSearch } from './form'
import { clearLocation } from './map'
import { setActiveItinerary } from './narrative'
-import { getUiUrlParams, getUrlParams } from '../util/query'
+import { getUiUrlParams } from '../util/state'
/**
* Wrapper function for history#push that preserves the current search or, if
@@ -103,7 +104,7 @@ export function handleBackButtonPress (e) {
const uiUrlParams = getUiUrlParams(otpState)
// Get new search ID from URL after back button pressed.
// console.log('back button pressed', e)
- const urlParams = getUrlParams()
+ const urlParams = coreUtils.query.getUrlParams()
const previousSearchId = urlParams.ui_activeSearch
const previousItinIndex = +urlParams.ui_activeItinerary || 0
const previousSearch = otpState.searches[previousSearchId]
diff --git a/lib/components/app/default-main-panel.js b/lib/components/app/default-main-panel.js
index ed3de24b6..e200c01ef 100644
--- a/lib/components/app/default-main-panel.js
+++ b/lib/components/app/default-main-panel.js
@@ -14,10 +14,11 @@ class DefaultMainPanel extends Component {
const {
activeSearch,
currentQuery,
- customIcons,
itineraryClass,
itineraryFooter,
+ LegIcon,
mainPanelContent,
+ ModeIcon,
showUserSettings
} = this.props
const showPlanTripButton = mainPanelContent === 'EDIT_DATETIME' ||
@@ -35,7 +36,7 @@ class DefaultMainPanel extends Component {
paddingBottom: 15,
overflow: 'auto'
}}>
-
+
{!activeSearch && !showPlanTripButton && showUserSettings &&
}
@@ -43,7 +44,8 @@ class DefaultMainPanel extends Component {
+ LegIcon={LegIcon}
+ />
{showPlanTripButton &&
diff --git a/lib/components/app/print-layout.js b/lib/components/app/print-layout.js
index d6d126747..c140345d8 100644
--- a/lib/components/app/print-layout.js
+++ b/lib/components/app/print-layout.js
@@ -1,20 +1,19 @@
-import React, { Component } from 'react'
+import PrintableItinerary from '@opentripplanner/printable-itinerary'
import PropTypes from 'prop-types'
-import { connect } from 'react-redux'
+import React, { Component } from 'react'
import { Button } from 'react-bootstrap'
+import { connect } from 'react-redux'
-import BaseMap from '../map/base-map'
-import EndpointsOverlay from '../map/endpoints-overlay'
-import TransitiveOverlay from '../map/transitive-overlay'
-import PrintableItinerary from '../narrative/printable/printable-itinerary'
import { parseUrlQueryString } from '../../actions/form'
import { routingQuery } from '../../actions/api'
+import DefaultMap from '../map/default-map'
+import TripDetails from '../narrative/connected-trip-details'
import { getActiveItinerary } from '../../util/state'
-import { getTimeFormat } from '../../util/time'
class PrintLayout extends Component {
static propTypes = {
itinerary: PropTypes.object,
+ LegIcon: PropTypes.elementType.isRequired,
parseQueryString: PropTypes.func
}
@@ -34,14 +33,14 @@ class PrintLayout extends Component {
}
componentDidMount () {
- const { location } = this.props
+ const { location, parseUrlQueryString } = this.props
// Add print-view class to html tag to ensure that iOS scroll fix only applies
// to non-print views.
const root = document.getElementsByTagName('html')[0]
root.setAttribute('class', 'print-view')
// Parse the URL query parameters, if present
if (location && location.search) {
- this.props.parseUrlQueryString()
+ parseUrlQueryString()
}
}
@@ -54,7 +53,7 @@ class PrintLayout extends Component {
}
render () {
- const { configCompanies, customIcons, itinerary, timeFormat } = this.props
+ const { config, itinerary, LegIcon } = this.props
return (
{/* The header bar, including the Toggle Map and Print buttons */}
@@ -74,21 +73,20 @@ class PrintLayout extends Component {
{/* The map, if visible */}
{this.state.mapVisible &&
-
-
-
-
+
}
{/* The main itinerary body */}
- {itinerary
- ?
- : null
+ {itinerary &&
+ <>
+
+
+ >
}
)
@@ -99,9 +97,8 @@ class PrintLayout extends Component {
const mapStateToProps = (state, ownProps) => {
return {
- itinerary: getActiveItinerary(state.otp),
- configCompanies: state.otp.config.companies,
- timeFormat: getTimeFormat(state.otp.config)
+ config: state.otp.config,
+ itinerary: getActiveItinerary(state.otp)
}
}
diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js
index 9705cd327..eea5f49a7 100644
--- a/lib/components/app/responsive-webapp.js
+++ b/lib/components/app/responsive-webapp.js
@@ -1,20 +1,21 @@
-import React, { Component } from 'react'
+import { ConnectedRouter } from 'connected-react-router'
+import { createHashHistory } from 'history'
+import isEqual from 'lodash.isequal'
+import coreUtils from '@opentripplanner/core-utils'
import PropTypes from 'prop-types'
+import React, { Component } from 'react'
import { connect } from 'react-redux'
-import isEqual from 'lodash.isequal'
import { Route, Switch, withRouter } from 'react-router'
-import { createHashHistory } from 'history'
-import { ConnectedRouter } from 'connected-react-router'
import PrintLayout from './print-layout'
import { setMapCenter, setMapZoom } from '../../actions/config'
-import { setLocationToCurrent } from '../../actions/map'
-import { getCurrentPosition, receivedPositionResponse } from '../../actions/location'
import { formChanged, parseUrlQueryString } from '../../actions/form'
+import { getCurrentPosition, receivedPositionResponse } from '../../actions/location'
+import { setLocationToCurrent } from '../../actions/map'
import { handleBackButtonPress, matchContentToUrl } from '../../actions/ui'
-import { getUrlParams } from '../../util/query'
-import { getTitle, isMobile } from '../../util/ui'
-import { getActiveItinerary } from '../../util/state'
+import { getActiveItinerary, getTitle } from '../../util/state'
+
+const { isMobile } = coreUtils.ui
class ResponsiveWebapp extends Component {
static propTypes = {
@@ -29,7 +30,7 @@ class ResponsiveWebapp extends Component {
componentDidUpdate (prevProps) {
const { currentPosition, location, query, title } = this.props
document.title = title
- const urlParams = getUrlParams()
+ const urlParams = coreUtils.query.getUrlParams()
const newSearchId = urlParams.ui_activeSearch
// Determine if trip is being replanned by checking the active search ID
// against the ID found in the URL params. If they are different, a new one
@@ -55,7 +56,7 @@ class ResponsiveWebapp extends Component {
// if in mobile mode and from field is not set, use current location as from and recenter map
if (isMobile() && this.props.query.from === null) {
- this.props.setLocationToCurrent({ type: 'from' })
+ this.props.setLocationToCurrent({ locationType: 'from' })
this.props.setMapCenter(pt)
if (this.props.initZoomOnLocate) {
this.props.setMapZoom({ zoom: this.props.initZoomOnLocate })
diff --git a/lib/components/form/checkbox-selector.js b/lib/components/form/checkbox-selector.js
deleted file mode 100644
index daabe0a6f..000000000
--- a/lib/components/form/checkbox-selector.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, {Component} from 'react'
-import PropTypes from 'prop-types'
-import { Form, FormGroup, Row, Col, Checkbox } from 'react-bootstrap'
-import { connect } from 'react-redux'
-
-import { setQueryParam } from '../../actions/form'
-
-class CheckboxSelector extends Component {
- static propTypes = {
- name: PropTypes.string,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.bool
- ]),
- label: PropTypes.string,
- setQueryParam: PropTypes.func
- }
-
- _onQueryParamChange = (evt) => {
- this.props.setQueryParam({ [this.props.name]: evt.target.checked })
- }
-
- render () {
- const { label } = this.props
- let value = this.props.value
- if (typeof value === 'string') value = (value === 'true')
-
- return (
-
- )}
-
- {/* The main panel for the selected routing type */}
- {rtDefaults.find(d => d.key === routingType).component}
+ `.
+ // These props are not relevant in modern browsers,
+ // where `` already
+ // formats the time|date according to the OS settings.
+ dateFormatLegacy={dateFormatLegacy}
+ timeFormatLegacy={timeFormatLegacy}
+ />
- {paramNames.map(param => {
- const paramInfo = queryParams.find(qp => qp.name === param)
- // Check that the parameter applies to the specified routingType
- if (!paramInfo.routingTypes.includes(query.routingType)) return
-
- // Check that the applicability test (if provided) is satisfied
- if (typeof paramInfo.applicable === 'function' &&
- !paramInfo.applicable(query, config)) return
-
- // Create the UI component based on the selector type
- switch (paramInfo.selector) {
- case 'DROPDOWN':
- return
- case 'CHECKBOX':
- return
- }
- })}
-
- )
- }
-}
-
-// connect to redux store
-
-const mapStateToProps = (state, ownProps) => {
- return {
- config: state.otp.config,
- query: state.otp.currentQuery
- }
-}
-
-const mapDispatchToProps = (dispatch, ownProps) => {
- return {
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(GeneralSettingsPanel)
diff --git a/lib/components/form/location-field.js b/lib/components/form/location-field.js
deleted file mode 100644
index 7c52c6e6b..000000000
--- a/lib/components/form/location-field.js
+++ /dev/null
@@ -1,605 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import ReactDOM from 'react-dom'
-import {
- Button,
- FormGroup,
- FormControl,
- InputGroup,
- DropdownButton,
- MenuItem
-} from 'react-bootstrap'
-import { connect } from 'react-redux'
-import { throttle } from 'throttle-debounce'
-
-import LocationIcon from '../icons/location-icon'
-import { setLocation, setLocationToCurrent, clearLocation } from '../../actions/map'
-import { addLocationSearch, getCurrentPosition } from '../../actions/location'
-import { findNearbyStops } from '../../actions/api'
-import { distanceStringImperial } from '../../util/distance'
-import getGeocoder from '../../util/geocoder'
-import { formatStoredPlaceName } from '../../util/map'
-import { getActiveSearch, getShowUserSettings } from '../../util/state'
-import { isIE } from '../../util/ui'
-
-class LocationField extends Component {
- static propTypes = {
- config: PropTypes.object,
- currentPosition: PropTypes.object,
- hideExistingValue: PropTypes.bool,
- location: PropTypes.object,
- label: PropTypes.string,
- nearbyStops: PropTypes.array,
- sessionSearches: PropTypes.array,
- showClearButton: PropTypes.bool,
- static: PropTypes.bool, // show autocomplete options as fixed/inline element rather than dropdown
- stopsIndex: PropTypes.object,
- type: PropTypes.string, // replace with locationType?
-
- // callbacks
- onClick: PropTypes.func,
- onLocationSelected: PropTypes.func,
-
- // dispatch
- addLocationSearch: PropTypes.func,
- clearLocation: PropTypes.func,
- setLocation: PropTypes.func,
- setLocationToCurrent: PropTypes.func
- }
-
- static defaultProps = {
- showClearButton: true
- }
-
- constructor (props) {
- super(props)
- this.state = {
- value: props.location && !props.hideExistingValue
- ? props.location.name
- : '',
- menuVisible: false,
- geocodedFeatures: [],
- activeIndex: null
- }
- }
-
- componentDidUpdate (prevProps) {
- // If location is updated externally, replace value and geocoded features
- // in internal state.
- // TODO: This might be considered an anti-pattern. There may be a more
- // effective way to handle this.
- const { location } = this.props
- if (location !== prevProps.location) {
- this.setState({
- value: location !== null ? location.name : '',
- geocodedFeatures: []
- })
- }
- }
-
- _geocodeAutocomplete = throttle(1000, (text) => {
- if (!text) {
- console.warn('No text entry provided for geocode autocomplete search.')
- return
- }
- getGeocoder(this.props.config.geocoder)
- .autocomplete({ text })
- .then((result) => {
- this.setState({ geocodedFeatures: result.features })
- }).catch((err) => {
- console.error(err)
- })
- })
-
- _geocodeSearch (text) {
- if (!text) {
- console.warn('No text entry provided for geocode search.')
- return
- }
- getGeocoder(this.props.config.geocoder)
- .search({ text })
- .then((result) => {
- if (result.features && result.features.length > 0) {
- // Only replace geocode items if results were found
- this.setState({ geocodedFeatures: result.features })
- } else {
- console.warn('No results found for geocode search. Not replacing results.')
- }
- }).catch((err) => {
- console.error(err)
- })
- }
-
- _getFormControlClassname () {
- return this.props.type + '-form-control'
- }
-
- _onClearButtonClick = () => {
- const { type } = this.props
- this.props.clearLocation({ type })
- this.setState({
- value: '',
- geocodedFeatures: []
- })
- ReactDOM.findDOMNode(this.formControl).focus()
- this._onTextInputClick()
- }
-
- _onDropdownToggle = (v, e) => {
- // if clicked on input form control, keep dropdown open; otherwise, toggle
- const targetIsInput =
- e.target.className.indexOf(this._getFormControlClassname()) !== -1
- const menuVisible = targetIsInput ? true : !this.state.menuVisible
- this.setState({ menuVisible })
- }
- /**
- * Only hide menu if the target clicked is not a menu item in the dropdown.
- * Otherwise, the click will not "finish" and the menu will hide without the
- * user having made a selection.
- */
- _onBlurFormGroup = (e) => {
- // IE does not use relatedTarget, so this check handles cross-browser support.
- // see https://stackoverflow.com/a/49325196/915811
- const target = e.relatedTarget !== null ? e.relatedTarget : document.activeElement
- if (!this.props.location && (!target || target.getAttribute('role') !== 'menuitem')) {
- this.setState({ menuVisible: false, value: '', geocodedFeatures: [] })
- }
- }
-
- _onTextInputChange = (evt) => {
- this.setState({ value: evt.target.value, menuVisible: true })
- this._geocodeAutocomplete(evt.target.value)
- }
-
- _onTextInputClick = () => {
- const { config, currentPosition, nearbyStops, onClick } = this.props
- if (typeof onClick === 'function') onClick()
- this.setState({ menuVisible: true })
- if (nearbyStops.length === 0 && currentPosition && currentPosition.coords) {
- this.props.findNearbyStops({
- lat: currentPosition.coords.latitude,
- lon: currentPosition.coords.longitude,
- max: config.geocoder.maxNearbyStops || 4
- })
- }
- }
-
- _onKeyDown = (evt) => {
- const { activeIndex, menuVisible } = this.state
- switch (evt.key) {
- // 'Down' arrow key pressed: move selected menu item down by one position
- case 'ArrowDown':
- // Suppress default 'ArrowDown' behavior which moves cursor to end
- evt.preventDefault()
- if (!menuVisible) {
- // If the menu is not visible, simulate a text input click to show it.
- return this._onTextInputClick()
- }
- if (activeIndex === this.menuItemCount - 1) {
- return this.setState({ activeIndex: null })
- }
- return this.setState({
- activeIndex: activeIndex === null
- ? 0
- : activeIndex + 1
- })
-
- // 'Up' arrow key pressed: move selection up by one position
- case 'ArrowUp':
- // Suppress default 'ArrowUp' behavior which moves cursor to beginning
- evt.preventDefault()
- if (activeIndex === 0) {
- return this.setState({ activeIndex: null })
- }
- return this.setState({
- activeIndex: activeIndex === null
- ? this.menuItemCount - 1
- : activeIndex - 1
- })
-
- // 'Enter' keypress serves two purposes:
- // - If pressed when typing in search string, switch from 'autocomplete'
- // to 'search' geocoding
- // - If pressed when dropdown results menu is active, apply the location
- // associated with current selected menu item
- case 'Enter':
- if (typeof activeIndex === 'number') { // Menu is active
- // Retrieve location selection handler from lookup object and invoke
- const locationSelected = this.locationSelectedLookup[activeIndex]
- if (locationSelected) locationSelected()
-
- // Clear selection & hide the menu
- this.setState({
- menuVisible: false,
- activeIndex: null
- })
- } else { // Menu not active; get geocode 'search' results
- this._geocodeSearch(evt.target.value)
- // Ensure menu is visible.
- this.setState({ menuVisible: true })
- }
-
- // Suppress default 'Enter' behavior which causes page to reload
- evt.preventDefault()
- break
- case 'Escape':
- // Clear selection & hide the menu
- return this.setState({
- menuVisible: false,
- activeIndex: null
- })
- // Any other key pressed: clear active selection
- default:
- return this.setState({ activeIndex: null })
- }
- }
-
- _setLocation (location) {
- const { onLocationSelected, setLocation, type } = this.props
- onLocationSelected && onLocationSelected()
- setLocation({ type, location })
- }
-
- _useCurrentLocation = () => {
- const {
- currentPosition,
- getCurrentPosition,
- onLocationSelected,
- setLocationToCurrent,
- type
- } = this.props
- if (currentPosition.coords) {
- // We already have geolocation coordinates
- setLocationToCurrent({ type })
- onLocationSelected && onLocationSelected()
- } else {
- // Call geolocation.getCurrentPosition and set as from/to type
- this.setState({ fetchingLocation: true })
- getCurrentPosition(type, onLocationSelected)
- }
- }
-
- /**
- * Provide alert to user with reason for geolocation error
- */
- _geolocationAlert = () => {
- window.alert(
- `Geolocation either has been disabled for ${window.location.host} or is not available in your browser.\n\nReason: ${this.props.currentPosition.error.message || 'Unknown reason'}`
- )
- }
-
- render () {
- const {
- currentPosition,
- label,
- location,
- user,
- showClearButton,
- showUserSettings,
- static: isStatic,
- suppressNearby,
- type,
- nearbyStops
- } = this.props
- const locations = [...user.locations, ...user.recentPlaces]
- const { activeIndex } = this.state
- let geocodedFeatures = this.state.geocodedFeatures
- if (geocodedFeatures.length > 5) geocodedFeatures = geocodedFeatures.slice(0, 5)
-
- let sessionSearches = this.props.sessionSearches
- if (sessionSearches.length > 5) sessionSearches = sessionSearches.slice(0, 5)
-
- // Assemble menu contents, to be displayed either as dropdown or static panel.
- // Menu items are created in four phases: (1) the current location, (2) any
- // geocoder search results; (3) nearby transit stops; and (4) saved searches
-
- let menuItems = [] // array of menu items for display (may include non-selectable items e.g. dividers/headings)
- let itemIndex = 0 // the index of the current location-associated menu item (excluding non-selectable items)
- this.locationSelectedLookup = {} // maps itemIndex to a location selection handler (for use by the _onKeyDown method)
-
- /* 1) Process geocode search result option(s) */
- if (geocodedFeatures.length > 0) {
- // Add the menu sub-heading (not a selectable item)
- // menuItems.push()
-
- // Iterate through the geocoder results
- menuItems = menuItems.concat(geocodedFeatures.map((feature, i) => {
- // Create the selection handler
- const locationSelected = () => {
- getGeocoder(this.props.config.geocoder)
- .getLocationFromGeocodedFeature(feature)
- .then(location => {
- // Set the current location
- this._setLocation(location)
- // Add to the location search history
- this.props.addLocationSearch({ location })
- })
- }
-
- // Add to the selection handler lookup (for use in _onKeyDown)
- this.locationSelectedLookup[itemIndex] = locationSelected
-
- // Create and return the option menu item
- const option = createOption('map-pin', feature.properties.label, locationSelected, itemIndex === activeIndex, i === geocodedFeatures.length - 1)
- itemIndex++
- return option
- }))
- }
-
- /* 2) Process nearby transit stop options */
- if (nearbyStops.length > 0 && !suppressNearby) {
- // Add the menu sub-heading (not a selectable item)
- menuItems.push()
-
- // Iterate through the found nearby stops
- menuItems = menuItems.concat(nearbyStops.map((stopId, i) => {
- // Constuct the location
- const stop = this.props.stopsIndex[stopId]
- const location = {
- name: stop.name,
- lat: stop.lat,
- lon: stop.lon
- }
-
- // Create the location selected handler
- const locationSelected = () => { this._setLocation(location) }
-
- // Add to the selection handler lookup (for use in _onKeyDown)
- this.locationSelectedLookup[itemIndex] = locationSelected
-
- // Create and return the option menu item
- const option = createTransitStopOption(stop, locationSelected, itemIndex === activeIndex, i === nearbyStops.length - 1)
- itemIndex++
- return option
- }))
- }
-
- /* 3) Process recent search history options */
- if (sessionSearches.length > 0) {
- // Add the menu sub-heading (not a selectable item)
- menuItems.push()
-
- // Iterate through any saved locations
- menuItems = menuItems.concat(sessionSearches.map((location, i) => {
- // Create the location-selected handler
- const locationSelected = () => { this._setLocation(location) }
-
- // Add to the selection handler lookup (for use in _onKeyDown)
- this.locationSelectedLookup[itemIndex] = locationSelected
-
- // Create and return the option menu item
- const option = createOption('search', location.name, locationSelected, itemIndex === activeIndex, i === sessionSearches.length - 1)
- itemIndex++
- return option
- }))
- }
-
- /* 3b) Process stored user locations */
- if (locations.length > 0 && showUserSettings) {
- // Add the menu sub-heading (not a selectable item)
- menuItems.push()
-
- // Iterate through any saved locations
- menuItems = menuItems.concat(locations.map((location, i) => {
- // Create the location-selected handler
- const locationSelected = () => { this._setLocation(location) }
-
- // Add to the selection handler lookup (for use in _onKeyDown)
- this.locationSelectedLookup[itemIndex] = locationSelected
-
- // Create and return the option menu item
- const option = createOption(
- location.icon,
- formatStoredPlaceName(location),
- locationSelected,
- itemIndex === activeIndex,
- i === locations.length - 1
- )
- itemIndex++
- return option
- }))
- }
-
- /* 4) Process the current location */
- let locationSelected, optionIcon, optionTitle
-
- if (!currentPosition.error) { // current position detected successfully
- locationSelected = this._useCurrentLocation
- optionIcon = 'location-arrow'
- optionTitle = 'Use Current Location'
- } else { // error detecting current position
- locationSelected = this._geolocationAlert
- optionIcon = 'ban'
- optionTitle = 'Current location not available'
- }
-
- // Add to the selection handler lookup (for use in _onKeyDown)
- this.locationSelectedLookup[itemIndex] = locationSelected
-
- if (!suppressNearby) {
- // Create and add the option item to the menu items array
- const currentLocationOption = createOption(
- optionIcon,
- optionTitle,
- locationSelected,
- itemIndex === activeIndex
- )
- menuItems.push(currentLocationOption)
- itemIndex++
- }
-
- // Store the number of location-associated items for reference in the _onKeyDown method
- this.menuItemCount = itemIndex
-
- /** the text input element **/
- const placeholder = currentPosition.fetching === type
- ? 'Fetching location...'
- : label || type
- const textControl = { this.formControl = ctl }}
- className={this._getFormControlClassname()}
- type='text'
- value={this.state.value}
- placeholder={placeholder}
- onChange={this._onTextInputChange}
- onClick={this._onTextInputClick}
- onKeyDown={this._onKeyDown}
- />
-
- // Only include the clear ('X') button add-on if a location is selected
- // or if the input field has text.
- const clearButton = showClearButton && location
- ?
-
-
- : null
- if (isStatic) {
- // 'static' mode (menu is displayed alongside input, e.g., for mobile view)
- return (
-
-
-
- {menuItems.length > 0 // Show typing prompt to avoid empty screen
- ? menuItems
- :
- }
-