Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create child views managing list of Predictions #2292

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions src/js/models/geocoder/Prediction.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ define(['backbone'], (Backbone) => {
* @since x.x.x
*/
const Prediction = Backbone.Model.extend({
/**
* Overrides the default Backbone.Model.defaults() function to specify
* default attributes for the Map
* @name Prediction#defaults
* @type {Object}
* @property {string} description A user-friendly description of a Google
* Maps Place.
* @property {string} googleMapsPlaceId Unique identifier that can be
* geocoded by the Google Maps Geocoder API.
*/
/**
* Overrides the default Backbone.Model.defaults() function to specify
* default attributes for the Map
* @name Prediction#defaults
* @type {Object}
* @property {string} description A user-friendly description of a Google
* Maps Place.
* @property {string} googleMapsPlaceId Unique identifier that can be
* geocoded by the Google Maps Geocoder API.
*/
defaults() {
return { description: '', googleMapsPlaceId: '' };
},
Expand Down
173 changes: 173 additions & 0 deletions src/js/models/maps/viewfinder/ViewfinderModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use strict';
define(
[
'underscore',
'backbone',
'cesium',
'models/geocoder/GeocoderSearch',
'models/maps/GeoPoint'],
(_, Backbone, Cesium, GeocoderSearch, GeoPoint) => {
const NO_RESULTS_MESSAGE = 'No search results found.';
/**
* @class ViewfinderModel
* @classdes ViewfinderModel maintains state for the ViewfinderView and
* interfaces with location searching services.
* @classcategory Models/Maps
*/
const ViewfinderModel = Backbone.Model.extend({
/**
* @name ViewfinderModel#defaults
* @type {Object}
* @property {string} error is the current error string to be displayed
* in the UI.
* @property {number} focusIndex is the index of the element
* in the list of predictions that shoudl be highlighted as focus.
* @property {Prediction[]} predictions a list of Predictions models that
* correspond to the user's search query.
* @property {string} query the user's search query.
*/
defaults() {
return {
error: '',
focusIndex: -1,
predictions: [],
query: '',
}
},

/**
* @param {Map} mapModel is the Map model that the ViewfinderModel is
* managing for the corresponding ViewfinderView.
*/
initialize({ mapModel }) {
this.geocoderSearch = new GeocoderSearch();
this.mapModel = mapModel;
},

/**
* Get autocompletion predictions from the GeocoderSearch model.
* @param {string} rawQuery is the user's search query with spaces.
*/
async autocompleteSearch(rawQuery) {
const query = rawQuery.trim();
if (this.get('query') === query) {
return;
} else if (!query) {
this.set({ error: '', predictions: [], query: '', focusIndex: -1, });
return;
} else if (GeoPoint.couldBeLatLong(query)) {
this.set({ predictions: [], query: '', focusIndex: -1, });
return;
}

// User is looking for autocompletions.
const predictions = await this.geocoderSearch.autocomplete(query);
const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : '';
this.set({ error, focusIndex: -1, predictions, query, });
},

/**
* Decrement the focused index with a minimum value of 0. This corresponds
* to an ArrowUp key down event.
* Note: An ArrowUp key press while the current index is -1 will
* result in highlighting the first element in the list.
*/
decrementFocusIndex() {
const currentIndex = this.get('focusIndex');
this.set('focusIndex', Math.max(0, currentIndex - 1));
},

/**
* Increment the focused index with a maximum value of the last value in
* the list. This corresponds to an ArrowDown key down event.
*/
incrementFocusIndex() {
const currentIndex = this.get('focusIndex');
this.set(
'focusIndex',
Math.min(currentIndex + 1, this.get('predictions').length - 1)
);
},

/**
* Reset the focused index back to the initial value so that no element
* in the UI is highlighted.
*/
resetFocusIndex() {
this.set('focusIndex', -1);
},

/**
* Navigate to the GeocodedLocation.
* @param {GeocodedLocation} geocoding is the location that corresponds
* to the the selected prediction.
*/
goToLocation(geocoding) {
if (!geocoding) return;

const coords = geocoding.get('box').getCoords();
this.mapModel.zoomTo({
destination: Cesium.Rectangle.fromDegrees(
coords.west,
coords.south,
coords.east,
coords.north,
)
});
},

/**
* Select a prediction from the list of predictions and navigate there.
* @param {Prediction} prediction is the user-selected Prediction that
* needs to be geocoded and navigated to.
*/
async selectPrediction(prediction) {
if (!prediction) return;

const geocodings = await this.geocoderSearch.geocode(prediction);

if (geocodings.length === 0) {
this.set('error', NO_RESULTS_MESSAGE)
return;
}

this.trigger('selection-made', prediction.get('description'));
this.goToLocation(geocodings[0]);
},

/**
* Event handler for Backbone.View configuration that is called whenever
* the user clicks the search button or hits the Enter key.
* @param {string} value is the query string.
*/
async search(value) {
// This is not a lat,long value, so geocode the prediction instead.
if (!GeoPoint.couldBeLatLong(value)) {
const focusedIndex = Math.max(0, this.get("focusIndex"));
this.selectPrediction(this.get('predictions')[focusedIndex]);
return;
}

try {
const geoPoint = new GeoPoint(value, { parse: true });
geoPoint.set("height", 10000 /* meters */);
if (geoPoint.isValid()) {
this.set('error', '');
this.mapModel.zoomTo(geoPoint);
ianguerin marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const errors = geoPoint.validationError;
if (errors.latitude) {
this.set('error', errors.latitude);
} else if (errors.longitude) {
this.set('error', errors.longitude);
}
} catch (e) {
this.set('error', e.message);
}
},
});

return ViewfinderModel;
});
9 changes: 9 additions & 0 deletions src/js/templates/maps/viewfinder/viewfinder-prediction.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="<%= classNames.content %> <%
if(isFocused) {
%>viewfinder-prediction__focused<%
} %>">
<i class="icon-map-marker"></i>
<div class="viewfinder-prediction__main" title="<%= description %>">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting- is title for accessibility?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This provides a mechanism for the user to see the entire prediction description, since it will overflow with ellipsis if the string is too long
ellipses

<%= description %>
</div>
</div>
113 changes: 113 additions & 0 deletions src/js/views/maps/viewfinder/PredictionView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';
define(
['backbone', 'text!templates/maps/viewfinder/viewfinder-prediction.html'],
(Backbone, Template) => {
// The base classname to use for this View's template elements.
const BASE_CLASS = 'viewfinder-prediction';

/**
* @class PredictionView
* @classdesc PredictionView shows an autocomplete suggestion
* for the user when they are searching for a place on a map.
* @classcategory Views/Maps
* @name PredictionView
* @extends Backbone.View
* @screenshot views/maps/viewfinder/PredictionView.png
* @since x.x.x
* @constructs PredictionView
*/
const PredictionView = Backbone.View.extend({
/**
* The type of View this is
* @type {string}
*/
type: 'PredictionView',

/**
* The HTML class to use for this view's outermost element.
* @type {string}
*/
className: BASE_CLASS,

/**
* The HTML element to use for this view's outermost element.
* @type {string}
*/
tagName: 'li',

/**
* The HTML classes to use for this view's HTML elements.
* @type {Object<string,string>}
*/
classNames: {
content: `${BASE_CLASS}__content`,
},

/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: { click: 'select' },
ianguerin marked this conversation as resolved.
Show resolved Hide resolved

/**
* Values meant to be used by the rendered HTML template.
*/
templateVars: {
classNames: {},
isFocused: false,
},

/**
* @typedef {Object} PredictionViewOptions
* @property {Prediction} The Prediction model associated with this
* autocompletion prediction.
* @property {ViewfinderModel} The model associated with the parent view.
* @property {number} The position of this prediction within the parent's
* full list of predictions.
*/
initialize({ index, predictionModel, viewfinderModel }) {
this.predictionModel = predictionModel;
this.viewfinderModel = viewfinderModel;
this.index = index;

this.templateVars = {
...this.templateVars,
classNames: this.classNames,
description: this.predictionModel.get('description'),
};

this.setupListeners();
},

/**
* Setup all event listeners on ViewfinderModel.
*/
setupListeners() {
this.listenTo(this.viewfinderModel, 'change:focusIndex', () => {
this.render();
});
},

/**
* Event handler function that selects this element, deselecting any other
* sibling list elements.
*/
select(event) {
this.viewfinderModel.selectPrediction(this.predictionModel);
},

/**
* Render the view by updating the HTML of the element.
* The new HTML is computed from an HTML template that
* is passed an object with relevant view state.
* */
render() {
const focusIndex = this.viewfinderModel.get('focusIndex');
this.templateVars.isFocused = focusIndex === this.index;

this.el.innerHTML = _.template(Template)(this.templateVars);
},
});

return PredictionView;
});
Loading
Loading