/** * @module nyc/ol/FinderApp */ import $ from 'jquery' import nyc from 'nyc' import MapMgr from 'nyc/ol/MapMgr' import Dialog from 'nyc/Dialog' import Share from 'nyc/Share' import Tabs from 'nyc/Tabs' import Directions from 'nyc/Directions' import Translate from 'nyc/lang/Translate' import Goog from 'nyc/lang/Goog' import Filters from 'nyc/ol/Filters' /** * @desc A class that provides a template for creating basic finder apps * @public * @class */ class FinderApp extends MapMgr { /** * @desc Create an instance of FinderApp * @public * @constructor * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options */ constructor(options) { $('body').append(FinderApp.HTML).addClass('fnd') options.listTarget = '#facilities div' options.mapTarget = '#map' options.mouseWheelZoom = options.mouseWheelZoom === undefined ? true : options.mouseWheelZoom super(options) global.finderApp = this this.pager.find('h2.info').addClass('screen-reader-only') const title = $('<div></div>').html(options.title) nyc.noSpaceBarScroll() $('#banner').html(title) $('#home').attr('title', title.text()) $('.fnd #home').on('click', () => document.location.reload()) /** * @desc The filters for filtering the facilities * @public * @member {module:nyc/ol/Filters~Filters} */ this.filters = this.createFilters(options.filterChoiceOptions) /** * @desc The tabs containing the map, facilities and filters * @public * @member {module:nyc/Tabs~Tabs} */ this.tabs = this.createTabs(options) $('#map').attr('tabindex', -1) this.adjustTabs() this.showSplash(options.splashOptions) new Share({target: '#map'}) new Goog({ target: '#map', languages: options.languages || Translate.DEFAULT_LANGUAGES, button: true }) /** * @private * @member {module:nyc/Directions~Directions} */ this.directions = null /** * @private * @member {string} */ this.directionsUrl = options.directionsUrl /** * @private * @member {google.maps.TravelMode} */ this.defaultDirectionsMode = options.defaultDirectionsMode this.pager.on('change', this.setFacilitiesLabel) } /** * @desc Reset the facilities list * @public * @method * @param {Object} event Event object */ resetList(event) { super.resetList(event) if (event instanceof Filters) { $('#tabs .btns .btn-2').addClass('filtered') } } /** * @public * @override * @method * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options * @return {ol.format.Feature} The parent format */ createParentFormat(options) { return options.facilityFormat.parentFormat || options.facilityFormat } /** * @public * @override * @method * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options * @param {ol.format.Feature} format The OpenLayers feature format * @return {Array<Object<string, fuction>>} Decorations */ createDecorations(options) { const format = options.facilityFormat let decorations = [MapMgr.FEATURE_DECORATIONS, {app: this}] if (format.parentFormat && format.parentFormat.decorations) { decorations = decorations.concat(format.parentFormat.decorations) } if (format.decorations) { decorations = decorations.concat(format.decorations) } if (options.decorations) { decorations = decorations.concat(options.decorations) } return decorations } /** * @desc Centers and zooms to the provided feature * @public * @method * @param {ol.Feature} feature OpenLayers feature */ zoomTo(feature) { super.zoomTo(feature) if ($('#tabs .btns h2:first-of-type').css('display') !== 'none') { this.tabs.open('#map') } } /** * @desc Handles geocoded and geolocated events * @access protected * @method * @param {module:nyc/Locator~Locator.Result} location Location */ located(location) { super.located(location) this.focusFacilities() } /** * @desc Provides directions to the provided facility feature * @public * @override * @method * @param {ol.Feature} feature OpenLayers feature * @param {JQuery} returnFocus The DOM element that should receive focus when leaving the directions view */ directionsTo(feature, returnFocus) { this.directions = this.directions || new Directions({ url: this.directionsUrl, toggle: '#tabs', mode: this.defaultDirectionsMode }) const to = feature.getFullAddress() const name = feature.getName() const from = this.getFromAddr() this.directions.directions({ from: from, to: to, facility: name, origin: this.location, destination: { name: feature.getName(), coordinate: feature.getGeometry().getCoordinates() }, returnFocus: returnFocus }) } /** * @desc Creates the filters for the facility features * @access protected * @method * @param {Array<module:nyc/ol/Filters~Filters.ChoiceOptions>=} choiceOptions Choice options * @return {module:nyc/ol/Filters~Filters} Filters */ createFilters(choiceOptions) { if (choiceOptions) { const me = this $('#filters').empty() const apply = $('<button class="apply">Apply</button>') const filters = new Filters({ target: '#filters', source: me.source, choiceOptions: choiceOptions }) filters.on('change', me.resetList, me) $('#filters').append(apply) apply.on('click tap keypress', () => { me.focusFacilities(true) }) return filters } } /** * @desc Creates the tabs for the map, facilities and filters * @access protected * @method * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Options * @return {module:nyc/Tabs~Tabs} Tabs */ createTabs(options) { const pages = [ {tab: '#map', title: 'Map'}, {tab: '#facilities', title: options.facilityTabTitle || 'Facilities'} ] if (options.splashOptions) { $('#tabs').attr('aria-hidden', true) } if (this.filters) { pages.push({tab: '#filters', title: options.filterTabTitle || 'Filters'}) } const tabs = new Tabs({target: '#tabs', tabs: pages}) tabs.on('change', this.tabChange, this) $(window).resize($.proxy(this.adjustTabs, this)) return tabs } /** * @private * @method */ setFacilitiesLabel() { $('#facilities>div[role="region"]').attr( 'aria-label', $('#facilities h2.info').html() ) } /** * @private * @method * @returns {boolean} The display state */ isMobile() { return $('#tabs .btns>h2:first-of-type').css('display') === 'block' } /** * @private * @method * @returns {module:nyc/Dialog~Dialog.ShowOptions} Dialog options */ mobileDiaOpts() { const location = this.location const locationName = location.name const feature = this.source.sort(location.coordinate)[0] const distance = feature.distanceHtml(true).html() return { message: `<strong>${feature.getName()}</strong><br> is located ${distance} from your location<br> <strong>${locationName}</strong>`, buttonText: [ `View ${$('#tab-btn-1').html()} list`, 'View the map' ] } } /** * @private * @method * @param {boolean} applyBtn The filter apply button was pressed by a screen reader user */ focusFacilities(applyBtn) { const tabs = this.tabs this.setFacilitiesLabel() if (!applyBtn && this.isMobile()) { const options = this.mobileDiaOpts() new Dialog({css: 'shw-lst'}) .yesNo(options).then(showFacilities => { if (showFacilities) { tabs.open('#facilities') } }) } else { tabs.open('#facilities') } } /** * @private * @method * @param {module:nyc/Dialog~Dialog.Options} options Dialog options */ showSplash(options) { const input = this.locationMgr.search.input if (options) { options.buttonText = options.buttonText || ['Continue'] new Dialog({css: 'splash'}).ok(options).then(() => { $('#tabs').attr('aria-hidden', false) input.focus() }) } else { input.focus() } } /** * @private * @method * @return {boolean} True if the tabs fill the screen */ tabsFillScreen() { return Math.abs(this.tabs.getContainer().width() - $(window).width()) < 1 } /** * @private * @method */ adjustTabs() { /* * when input gets focus screen resizes on android * causing input to lose focus when tabs are adjusted * so we don't adjust tabs when input has focus */ if ($('#directions').css('display') !== 'block' && !nyc.activeElement().isTextInput) { this.tabs.open(this.tabsFillScreen() ? '#map' : '#facilities') } } /** * @desc Handles the tab change event * @access protected * @method * @param {module:nyc/Tabs~Tabs} tabs Tabs */ tabChange(tabs) { if (!this.tabsFillScreen()) { $('#map').attr('aria-hidden', false) } this.map.setSize([$('#map').width(), $('#map').height()]) } } /** * @desc Constructor options for {@link module:nyc/ol/FinderApp~FinderApp} * @public * @typedef {Object} * @property {string} title The app title * @property {module:nyc/Dialog~Dialog.Options} splashOptions Content to display as a splash page * @property {string} [facilityTabTitle=Facilities] Title for the facilites list * @property {string} facilityUrl The URL for the facility features data * @property {ol.format.Feature=} facilityFormat The format of the facilities data * @property {ol.style.Style} facilityStyle The styling for the facilities layer * @property {module:nyc/Search~Search.FeatureSearchOptions|boolean} [facilitySearch=false] Search options for feature searches or true to use default search options * @property {string} [filterTabTitle=Filters] Title for the filters tab * @property {Array<module:nyc/ol/Filters~Filters.ChoiceOptions>=} filterChoiceOptions Filter definitions * @property {string} geoclientUrl The URL for the Geoclient geocoder with approriate keys * @property {string} directionsUrl The URL for the Google directions API with approriate keys * @property {google.maps.TravelMode} defaultDirectionsMode The mode for Google directions * @property {boolean} [mouseWheelZoom=true] Allow mouse wheel map zooming */ FinderApp.Options /** * @private * @const * @type {string} */ FinderApp.HTML = '<h1 id="banner" role="banner"></h1>' + '<a id="home" role="button" href="#">' + '<span class="screen-reader-only">Reload page</span>' + '<svg xmlns="http://www.w3.org/2000/svg" width="152" height="52">' + '<g transform="translate(1.5,0)">' + '<polygon points="15.5,1.2 3.1,1.2 0,4.3 0,47.7 3.1,50.8 15.5,50.8 18.6,47.7 18.6,35.3 34.1,50.8 46.6,50.8 49.7,47.7 49.7,4.3 46.6,1.2 34.1,1.2 31,4.3 31,16.7 "/>' + '<polygon points="83.8,47.7 83.8,38.4 99.3,22.9 99.3,10.5 99.3,4.3 96.2,1.2 83.8,1.2 80.7,4.3 80.7,10.5 74.5,16.7 68.3,10.5 68.3,4.3 65.2,1.2 52.8,1.2 49.7,4.3 49.7,22.9 65.2,38.4 65.2,47.7 68.3,50.8 80.7,50.8 "/>' + '<polygon points="145.9,29.1 130.4,29.1 130.4,32.2 118,32.2 118,19.8 130.4,19.8 130.4,22.9 145.9,22.9 149,19.8 149,10.5 139.7,1.2 108.6,1.2 99.3,10.5 99.3,41.5 108.6,50.8 139.7,50.8 149,41.5 149,32.2 "/>' + '</g>' + '</svg>' + '</a>' + '<div id="map"></div>' + '<div id="tabs"></div>' + '<div id="facilities"><div role="region"></div></div>' + '<div id="filters"></div>' export default FinderApp