@@ -100,7 +97,12 @@ class OtpRRExample extends Component {
/** mobile view **/
const mobileView = (
- )} title={(
OpenTripPlanner
)} />
+ }
+ title={
OpenTripPlanner
}
+ />
)
/** the main webapp **/
@@ -108,6 +110,7 @@ class OtpRRExample extends Component {
)
}
@@ -115,15 +118,18 @@ class OtpRRExample extends Component {
// render the app
render(
-
- { /**
+ (
+
+ { /**
* If not using router history, simply include OtpRRExample here:
* e.g.
*
*/
- }
-
+ }
+
+
+ )
+ ,
- ,
document.getElementById('root')
)
diff --git a/lib/actions/api.js b/lib/actions/api.js
index 705822ecb..1ac0b0c22 100644
--- a/lib/actions/api.js
+++ b/lib/actions/api.js
@@ -1,20 +1,24 @@
/* 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'
+import { getSecureFetchOptions } from '../util/middleware'
+
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')
@@ -79,7 +83,9 @@ function getActiveItinerary (otpState) {
*/
export function routingQuery (searchId = null) {
return async function (dispatch, getState) {
- const otpState = getState().otp
+ const state = getState()
+ const otpState = state.otp
+
const isNewSearch = !searchId
if (isNewSearch) searchId = randId()
const routingType = otpState.currentQuery.routingType
@@ -93,7 +99,7 @@ export function routingQuery (searchId = null) {
// fetch a realtime route
const query = constructRoutingQuery(otpState)
- fetch(query)
+ fetch(query, getOtpFetchOptions(state))
.then(getJsonAndCheckResponse)
.then(json => {
dispatch(routingResponse({ response: json, searchId }))
@@ -124,8 +130,29 @@ export function routingQuery (searchId = null) {
if (isNewSearch || params.ui_activeSearch !== searchId) {
dispatch(updateOtpUrlParams(otpState, searchId))
}
- // also fetch a non-realtime route
- fetch(constructRoutingQuery(otpState, true))
+
+ // Also fetch a non-realtime route.
+ //
+ // FIXME: The statement below may no longer apply with future work
+ // involving realtime info embedded in the OTP response.
+ // (That action records an entry again in the middleware.)
+ // For users who opted in to store trip request history,
+ // to avoid recording the same trip request twice in the middleware,
+ // only add the user Authorization token to the request
+ // when querying the non-realtime route.
+ //
+ // The advantage of using non-realtime route is that the middleware will be able to
+ // record and provide the theoretical itinerary summary without having to query OTP again.
+ // FIXME: Interestingly, and this could be from a side effect elsewhere,
+ // when a logged-in user refreshes the page, the trip request in the URL is not recorded again
+ // (state.user stays unpopulated until after this function is called).
+ //
+ const { user } = state
+ const storeTripHistory = user &&
+ user.loggedInUser &&
+ user.loggedInUser.storeTripHistory
+
+ fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, storeTripHistory))
.then(getJsonAndCheckResponse)
.then(json => {
// FIXME: This is only performed when ignoring realtimeupdates currently, just
@@ -148,6 +175,39 @@ function getJsonAndCheckResponse (res) {
return res.json()
}
+/**
+ * This method determines the fetch options (including API key and Authorization headers) for the OTP API.
+ * - If the OTP server is not the middleware server (standalone OTP server),
+ * an empty object is returned.
+ * - If the OTP server is the same as the middleware server,
+ * then an object is returned with the following:
+ * - A middleware API key, if it has been set in the configuration (it is most likely required),
+ * - An Auth0 accessToken, when includeToken is true and a user is logged in (userState.loggedInUser is not null).
+ * This method assumes JSON request bodies.)
+ */
+function getOtpFetchOptions (state, includeToken = false) {
+ let apiBaseUrl, apiKey, token
+
+ const { api, persistence } = state.otp.config
+ if (persistence && persistence.otp_middleware) {
+ ({ apiBaseUrl, apiKey } = persistence.otp_middleware)
+ }
+
+ const isOtpServerSameAsMiddleware = apiBaseUrl === api.host
+ if (isOtpServerSameAsMiddleware) {
+ if (includeToken && state.user) {
+ const { accessToken, loggedInUser } = state.user
+ if (accessToken && loggedInUser) {
+ token = accessToken
+ }
+ }
+
+ return getSecureFetchOptions(token, apiKey)
+ } else {
+ return {}
+ }
+}
+
function constructRoutingQuery (otpState, ignoreRealtimeUpdates) {
const { config, currentQuery } = otpState
const routingType = currentQuery.routingType
@@ -937,7 +997,7 @@ export function setUrlSearch (params, replaceCurrent = false) {
* is set correctly. Leaves any other existing URL parameters (e.g., UI) unchanged.
*/
export function updateOtpUrlParams (otpState, searchId) {
- const otpParams = getRoutingParams(otpState, true)
+ const otpParams = getRoutingParams(otpState)
return function (dispatch, getState) {
const params = {}
// Get all OTP-specific params, which will be retained unchanged in the URL
diff --git a/lib/actions/auth.js b/lib/actions/auth.js
new file mode 100644
index 000000000..cf5478e0f
--- /dev/null
+++ b/lib/actions/auth.js
@@ -0,0 +1,59 @@
+import { push } from 'connected-react-router'
+
+import { setPathBeforeSignIn } from '../actions/user'
+
+/**
+ * This function is called by the Auth0Provider component, with the described parameter(s),
+ * when a new access token could not be retrieved.
+ * @param {Error} err
+ * @param {AccessTokenRequestOptions} options
+ */
+export function showAccessTokenError (err, options) {
+ return function (dispatch, getState) {
+ // TODO: improve this.
+ console.error('Failed to retrieve access token: ', err)
+ }
+}
+
+/**
+ * This function is called by the Auth0Provider component, with the described parameter(s),
+ * when signing-in fails for some reason.
+ * @param {Error} err
+ */
+export function showLoginError (err) {
+ return function (dispatch, getState) {
+ // TODO: improve this.
+ if (err) dispatch(push('/oops'))
+ }
+}
+
+/**
+ * This function is called by the Auth0Provider component, with the described parameter(s),
+ * after the user signs in.
+ * @param {Object} appState The state that was stored when calling useAuth().login().
+ */
+export function processSignIn (appState) {
+ return function (dispatch, getState) {
+ if (appState && appState.urlHash) {
+ // At this stage after login, Auth0 has already redirected to /signedin (Auth0-whitelisted)
+ // which shows the AfterLoginScreen.
+ //
+ // Here, we save the URL hash prior to login (contains a combination of itinerary search, stop/trip view, etc.),
+ // so that the AfterLoginScreen can redirect back there when logged-in user info is fetched.
+ // (For routing, it is easier to deal with the path without the hash sign.)
+ const hashIndex = appState.urlHash.indexOf('#')
+ const urlHashWithoutHash = hashIndex >= 0
+ ? appState.urlHash.substr(hashIndex + 1)
+ : '/'
+ dispatch(setPathBeforeSignIn(urlHashWithoutHash))
+ } else if (appState && appState.returnTo) {
+ // TODO: Handle other after-login situations.
+ // Note that when redirecting from a login-protected (e.g. account) page while logged out,
+ // then returnTo is set by Auth0 to this object format:
+ // {
+ // pathname: "/"
+ // query: { ... }
+ // }
+ }
+ }
+}
diff --git a/lib/actions/form.js b/lib/actions/form.js
index 296579772..023e5ff17 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
planParamsToQuery(planParams, getState().otp.config)
.then(query => dispatch(setQueryParam(query, searchId)))
@@ -82,10 +81,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
@@ -104,7 +104,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
) {
@@ -116,8 +116,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 c4f386244..27cca104a 100644
--- a/lib/actions/ui.js
+++ b/lib/actions/ui.js
@@ -1,22 +1,25 @@
+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
+ * Wrapper function for history#push (or, if specified, replace, etc.)
+ * that preserves the current search or, if
* replaceSearch is provided (including an empty string), replaces the search
* when routing to a new URL path.
* @param {[type]} url path to route to
* @param {string} replaceSearch optional search string to replace current one
+ * @param {func} routingMethod the connected-react-router method to execute (defaults to push).
*/
-export function routeTo (url, replaceSearch) {
+export function routeTo (url, replaceSearch, routingMethod = push) {
return function (dispatch, getState) {
// Get search to preserve when routing to new path.
const { router } = getState()
@@ -27,7 +30,7 @@ export function routeTo (url, replaceSearch) {
} else {
path = `${path}${search}`
}
- dispatch(push(path))
+ dispatch(routingMethod(path))
}
}
@@ -109,7 +112,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/actions/user.js b/lib/actions/user.js
new file mode 100644
index 000000000..f306af6ae
--- /dev/null
+++ b/lib/actions/user.js
@@ -0,0 +1,107 @@
+import { createAction } from 'redux-actions'
+
+import { addUser, fetchUser, updateUser } from '../util/middleware'
+import { isNewUser } from '../util/user'
+
+const setCurrentUser = createAction('SET_CURRENT_USER')
+export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')
+
+function getStateForNewUser (auth0User) {
+ return {
+ auth0UserId: auth0User.sub,
+ email: auth0User.email,
+ hasConsentedToTerms: false, // User must agree to terms.
+ isEmailVerified: auth0User.email_verified,
+ notificationChannel: 'email',
+ phoneNumber: '',
+ recentLocations: [],
+ savedLocations: [],
+ storeTripHistory: false // User must opt in.
+ }
+}
+
+/**
+ * Fetches user preferences to state.user, or set initial values under state.user if no user has been loaded.
+ */
+export function fetchOrInitializeUser (auth) {
+ return async function (dispatch, getState) {
+ const { otp } = getState()
+ const { accessToken, user } = auth
+
+ try {
+ const result = await fetchUser(
+ otp.config.persistence.otp_middleware,
+ accessToken
+ )
+
+ // Beware! On AWS API gateway, if a user is not found in the middleware
+ // (e.g. they just created their Auth0 password but have not completed the account setup form yet),
+ // the call above will return, for example:
+ // {
+ // status: 'success',
+ // data: {
+ // "result": "ERR",
+ // "message": "No user with id=000000 found.",
+ // "code": 404,
+ // "detail": null
+ // }
+ // }
+ //
+ // The same call to a middleware instance that is not behind an API gateway
+ // will return:
+ // {
+ // status: 'error',
+ // message: 'Error get-ing user...'
+ // }
+ // TODO: Improve AWS response.
+
+ const resultData = result.data
+ const isNewAccount = result.status === 'error' || (resultData && resultData.result === 'ERR')
+
+ if (!isNewAccount) {
+ // TODO: Move next line somewhere else.
+ if (resultData.savedLocations === null) resultData.savedLocations = []
+ dispatch(setCurrentUser({ accessToken, user: resultData }))
+ } else {
+ dispatch(setCurrentUser({ accessToken, user: getStateForNewUser(user) }))
+ }
+ } catch (error) {
+ // TODO: improve error handling.
+ alert(`An error was encountered:\n${error}`)
+ }
+ }
+}
+
+/**
+ * Updates (or creates) a user entry in the middleware,
+ * then, if that was successful, updates the redux state with that user.
+ */
+export function createOrUpdateUser (userData) {
+ return async function (dispatch, getState) {
+ const { otp, user } = getState()
+ const { otp_middleware: otpMiddleware = null } = otp.config.persistence
+
+ if (otpMiddleware) {
+ const { accessToken, loggedInUser } = user
+
+ let result
+ if (isNewUser(loggedInUser)) {
+ result = await addUser(otpMiddleware, accessToken, userData)
+ } else {
+ result = await updateUser(otpMiddleware, accessToken, userData)
+ }
+
+ // TODO: improve the UI feedback messages for this.
+ if (result.status === 'success' && result.data) {
+ // Update application state with the user entry as saved
+ // (as returned) by the middleware.
+ const userData = result.data
+ dispatch(setCurrentUser({ accessToken, user: userData }))
+
+ alert('Your preferences have been saved.')
+ } else {
+ alert(`An error was encountered:\n${JSON.stringify(result)}`)
+ }
+ }
+ }
+}
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/desktop-nav.js b/lib/components/app/desktop-nav.js
new file mode 100644
index 000000000..558f166aa
--- /dev/null
+++ b/lib/components/app/desktop-nav.js
@@ -0,0 +1,81 @@
+import React from 'react'
+import { Nav, Navbar } from 'react-bootstrap'
+import { connect } from 'react-redux'
+
+import NavLoginButtonAuth0 from '../user/nav-login-button-auth0.js'
+import { accountLinks, getAuth0Config } from '../../util/auth'
+import { DEFAULT_APP_TITLE } from '../../util/constants'
+import AppMenu from './app-menu'
+
+/**
+ * The desktop navigation bar, featuring a `branding` logo or a `title` text
+ * defined in config.yml, and a sign-in button/menu with account links.
+ *
+ * The `branding` and `title` parameters in config.yml are handled
+ * and shown in this order in the navigation bar:
+ * 1. If `branding` is defined, it is shown, and no title is displayed.
+ * 2. If `branding` is not defined but if `title` is, then `title` is shown.
+ * 3. If neither is defined, just show 'OpenTripPlanner' (DEFAULT_APP_TITLE).
+ *
+ * TODO: merge with the mobile navigation bar.
+ */
+const DesktopNav = ({ otpConfig }) => {
+ const { branding, persistence, title = DEFAULT_APP_TITLE } = otpConfig
+ const showLogin = Boolean(getAuth0Config(persistence))
+
+ // Display branding and title in the order as described in the class summary.
+ let brandingOrTitle
+ if (branding) {
+ brandingOrTitle = (
+
+ )
+ } else {
+ brandingOrTitle = (
+
+
+ {brandingOrTitle}
+
+
+
+
+ {showLogin && (
+
+
+
+ )}
+
+ )
+}
+
+// connect to the redux store
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ otpConfig: state.otp.config
+ }
+}
+
+const mapDispatchToProps = {
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DesktopNav)
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 92d98be1f..4ab454a66 100644
--- a/lib/components/app/responsive-webapp.js
+++ b/lib/components/app/responsive-webapp.js
@@ -1,20 +1,29 @@
-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 { Auth0Provider } from 'use-auth0-hooks'
import PrintLayout from './print-layout'
+import * as authActions from '../../actions/auth'
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 { getAuth0Config } from '../../util/auth'
+import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from '../../util/constants'
+import { getActiveItinerary, getTitle } from '../../util/state'
+import AfterSignInScreen from '../user/after-signin-screen'
+import BeforeSignInScreen from '../user/before-signin-screen'
+import UserAccountScreen from '../user/user-account-screen'
+import withLoggedInUserSupport from '../user/with-logged-in-user-support'
+
+const { isMobile } = coreUtils.ui
class ResponsiveWebapp extends Component {
static propTypes = {
@@ -29,7 +38,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 +64,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 })
@@ -159,13 +168,38 @@ const mapDispatchToProps = {
const history = createHashHistory()
const WebappWithRouter = withRouter(
- connect(mapStateToProps, mapDispatchToProps)(ResponsiveWebapp)
+ withLoggedInUserSupport(
+ connect(mapStateToProps, mapDispatchToProps)(ResponsiveWebapp)
+ )
)
-class RouterWrapper extends Component {
+/**
+ * The routing component for the application.
+ * This is the top-most "standard" component,
+ * and we initialize the Auth0Provider here
+ * so that Auth0 services are available everywhere.
+ */
+class RouterWrapperWithAuth0 extends Component {
+ /**
+ * Combine the router props with the other props that get
+ * passed to the exported component. This way, it is possible for
+ * the PrintLayout, UserAccountScreen, and other components to
+ * receive the LegIcon or other needed props.
+ */
+ _combineProps = routerProps => {
+ return { ...this.props, ...routerProps }
+ }
+
render () {
- const { routerConfig } = this.props
- return (
+ const {
+ auth0Config,
+ processSignIn,
+ routerConfig,
+ showAccessTokenError,
+ showLoginError
+ } = this.props
+
+ const router = (
@@ -191,13 +225,29 @@ class RouterWrapper extends Component {
]}
render={() => }
/>
+ {
+ const props = this._combineProps(routerProps)
+ return
+ }}
+ />
+ {
+ const props = this._combineProps(routerProps)
+ return
+ }}
+ />
{
- // combine the router props with the other props that get
- // passed to the exported component. This way it's possible for
- // the PrintLayout component to receive the custom icons prop.
- const props = { ...this.props, ...routerProps }
+ component={routerProps => {
+ const props = this._combineProps(routerProps)
return
}}
/>
@@ -209,7 +259,38 @@ class RouterWrapper extends Component {
)
+
+ return (
+ auth0Config
+ ? (
+
+ {router}
+
+ )
+ : router
+ )
}
}
-const mapStateToWrapperProps = (state, ownProps) => ({ routerConfig: state.otp.config.reactRouter })
-export default connect(mapStateToWrapperProps)(RouterWrapper)
+
+const mapStateToWrapperProps = (state, ownProps) => ({
+ auth0Config: getAuth0Config(state.otp.config.persistence),
+ routerConfig: state.otp.config.reactRouter
+})
+
+const mapWrapperDispatchToProps = {
+ processSignIn: authActions.processSignIn,
+ showAccessTokenError: authActions.showAccessTokenError,
+ showLoginError: authActions.showLoginError
+}
+
+export default connect(mapStateToWrapperProps, mapWrapperDispatchToProps)(RouterWrapperWithAuth0)
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
- :
- }
-