Filter results from Google Autocomplete - google-maps-api-3

Is there a way to get the results from Google Autocomplete API before it's displayed below the input? I want to show results from any country except U.S.A.
I found this question: Google Maps API V3 - Anyway to retrieve Autocomplete results instead of dropdown rendering it? but it's not useful, because the method getQueryPredictions only returns 5 elements.
This is an example with UK and US Results: http://jsfiddle.net/LVdBK/
Is it possible?

I used the jquery autocomplete widget and called the google methods manually.
For our case, we only wanted to show addresses in Michigan, US.
Since Google doesn't allow filtering out responses to that degree you have to do it manually.
Override the source function of the jquery autocomplete
Call the google autocompleteService.getQueryPredictions method
Filter out the results you want and return them as the "response" callback of the jquery autocomplete.
Optionally, if you need more detail about the selected item from Google, override the select function of the jquery autocomplete and make a call to Google's PlacesService.getDetails method.
The below assumes you have the Google api reference with the "places" library.
<script src="https://maps.googleapis.com/maps/api/js?key=[yourKeyHere]&libraries=places&v=weekly" defer></script>
var _autoCompleteService; // defined globally in script
var _placesService; // defined globally in script
//...
// setup autocomplete wrapper for google places
// starting point in our city
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng('42.9655426','-85.6769166'),
new google.maps.LatLng('42.9655426','-85.6769166'));
if (_autoCompleteService == null) {
_autoCompleteService = new google.maps.places.AutocompleteService();
}
$("#CustomerAddress_Street").autocomplete({
minLength: 2,
source: function (request, response) {
if (request.term != '') {
var googleRequest = {
input: request.term,
bounds: defaultBounds,
types: ["geocode"],
componentRestrictions: { 'country': ['us'] },
fields: ['geometry', 'formatted_address']
}
_autoCompleteService.getQueryPredictions(googleRequest, function (predictions) {
var michiganOnly = new Array(); // array to hold only addresses in Michigan
for (var i = 0; i < predictions.length; i++) {
if (predictions[i].terms.length > 0) {
// find the State term. Could probably assume it's predictions[4], but not sure if it is guaranteed.
for (var j = 0; j < predictions[i].terms.length; j++) {
if (predictions[i].terms[j].value.length == 2) {
if (predictions[i].terms[j].value.toUpperCase() == 'MI') {
michiganOnly.push(predictions[i]);
}
}
}
}
}
response(michiganOnly);
});
}
},
select: function (event, ui) {
if (ui != null) {
var item = ui.item;
var request = {
placeId: ui.item.place_id
}
if (_placesService == null) {
$("body").append("<div id='GoogleAttribution'></div>"); // PlacesService() requires a field to put it's attribution image in. For now, just put on on the body
_placesService = new google.maps.places.PlacesService(document.getElementById('GoogleAttribution'));
}
_placesService.getDetails(request, function (result, status) {
if (result != null) {
const place = result;
if (!place.geometry) {
// User entered the name of a Place that was not suggested and
// pressed the Enter key, or the Place Details request failed.
//window.alert("No details available for input: '" + place.name + "'");
return;
}
else {
var latitude = place.geometry.location.lat();
var longitude = place.geometry.location.lng();
// do something with Lat/Lng
}
}
});
}
}
}).autocomplete("instance")._renderItem = function (ul, item) {
// item is the prediction object returned from our call to getQueryPredictions
// return the prediction object's "description" property or do something else
return $("<li>")
.append("<div>" + item.description + "</div>")
.appendTo(ul);
};
$("#CustomerAddress_Street").autocomplete("instance")._renderMenu = function (ul, items) {
// Google's terms require attribution, so when building the menu, append an item pointing to their image
var that = this;
$.each(items, function (index, item) {
that._renderItemData(ul, item);
});
$(ul).append("<li class='ui-menu-item'><div style='display:flex;justify-content:flex-end;'><img src='https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white3.png' /></div></li>")
}

Related

Copy Google Analytics custom definitions/dimensions between GA4 properties with the Google Analytics Data API?

I have a number of GA4 Google Analytics properties that need to share the same custom dimensions (custom definitions). Using the web UI to manually copy them is slow and error prone. How can I automate this process?
Using Google Apps Script and the Google Analytics Data API v1 I've created a function that almost does it:
https://script.google.com/d/1Tcf7L4nxU5zAdFiLB2GmVe2R20g33aiy0mhUd8Y851JLipluznoAWpB_/edit?usp=sharing
However the API is in beta and doesn't seem to have a way to add the dimensions to the destination GA property.
Does anyone have a way to complete this final step?
function main(){
gaSourcePropertyId = '12345678';
gaDestinationPropertyId = '87654321';
copyCustomDefinitions(gaSourcePropertyId, gaDestinationPropertyId);
}
function copyCustomDefinitions(gaSourcePropertyId, gaDestinationPropertyId) {
let sourceDimensions = getCustomDimensions(gaSourcePropertyId);
addCustomDimensions(gaDestinationPropertyId, sourceDimensions);
}
function getCustomDimensions(gaPropertyId) {
var sourceDimensions = AnalyticsData.Properties.getMetadata(`properties/${gaPropertyId}/metadata`)['dimensions'];
var sourceCustomDefinitions = [];
sourceDimensions.forEach((dimension)=>{
if (dimension['customDefinition'] == true){ sourceCustomDefinitions.push(dimension)}
});
return sourceCustomDefinitions;
}
function addCustomDimensions(gaPropertyId, dimensions) {
const destinationDimensions = getCustomDimensions(gaPropertyId);
const destinationDimensionApiNames = destinationDimensions.map(dimension=>dimension['apiName']);
dimensions.forEach((dimension)=>{
// Check if the new dimension already exists in the destination.
if ( !destinationDimensionApiNames.includes(dimension['apiName']) ) {
let newDimension = AnalyticsData.newDimension(dimension['apiName']); // The newDimension method doesn't seem to work yet!
// TODO - add the dimension to the destination property
console.warn('There is not yet an API function for adding dimensions to a property. Something like AnalyticsData.Properties.setMetadata is needed!');
} else {
console.info(`Dimension with apiName '${ dimension['apiName'] }' already present in destination! Dimension not added to destination`);
}
})
}
Turns out there is the Google Analytics Admin API which provides methods for manipulating the custom dimensions of a property.
This includes functions for listing custom dimensions and creating custom dimensions
function main(){
gaSourcePropertyId = '1234';
gaDestinationPropertyId = '4321';
copyCustomDimensions(gaSourcePropertyId, gaDestinationPropertyId);
}
function copyCustomDimensions(gaSourcePropertyId, gaDestinationPropertyId) {
let sourceDimensions = getCustomDimensions(gaSourcePropertyId);
addCustomDimensions(gaDestinationPropertyId, sourceDimensions);
}
function getCustomDimensions(gaPropertyId) {
try {
return AnalyticsAdmin.Properties.CustomDimensions.list(`properties/${gaPropertyId}`)['customDimensions'];
} catch (error) {
console.error(error);
}
}
function addCustomDimensions(gaPropertyId, dimensions) {
let destinationCustomDimensions = getCustomDimensions(gaPropertyId);
// The destination may not have any custom dimensions.
if (typeof destinationCustomDimensions == 'undefined') {
console.info(`Could not get custom definitions for property ID '${gaPropertyId}'.`, destinationCustomDimensions);
destinationCustomDimensions = [];
};
const destinationDimensionParameterNames = destinationCustomDimensions.map(dimension=>dimension['parameterName']);
dimensions.forEach((sourceDimension)=>{
// Check if the new dimension already exists in the destination.
if ( !destinationDimensionParameterNames.includes(sourceDimension['parameterName']) ) {
let destinationDimension = {
"parameterName": sourceDimension['parameterName'],
"displayName": sourceDimension['displayName'],
"description": sourceDimension['description'],
"scope": sourceDimension['scope']
};
let result = null;
try {
result = AnalyticsAdmin.Properties.CustomDimensions.create(destinationDimension, `properties/${gaPropertyId}`);
console.log('[COPIED]',result);
} catch (error) {
console.log('[FAILED]', destinationDimension)
console.error(error);
}
} else {
console.info(`[NO ACTION] Dimension with apiName '${ sourceDimension['parameterName'] }' already present in destination! Dimension not added to destination.`, sourceDimension);
}
});
}

Dynamically toggle resource column visibility

I have a FullCalendar scheduler on a webapp which has 2 way databinding for resources and events, all working great. I want to be able to present the user with a dropdown that enables them to toggle the visibility of a column, ideally completely client side.
I have tried a combination of addResource / removeResource however my issue here is that a rerender of the calendar (e.g. when a new event is added) then displays the previously removed resource. I can work around this however would prefer a really simple approach using JS / CSS. I currently cannot find a way to set a resource to not be visible, or to have zero width - is this possible?
There is an easy way to do this:
Store resources in an array variable resourceData.
Create another array called visibleResourceIds to store the ids of any resources you want to show.
In the resources callback function, filter resourceData to only contain the resources where the resource id exists in visibleResourceIds. Return the filtered array and fullcalendar will only add the desired resources for you.
To remove a resource from view, simply remove the resource id from visibleResourceIds and refetchResources. To add the resource back in, add the id to visibleResourceIds and refetchResources. DONE.
JSFiddle
var resourceData = [
{id: "1", title: "R1"},
{id: "2", title: "R2"},
{id: "3", title: "R3"}
];
var visibleResourceIds = ["1", "2", "3"];
// Your button/dropdown will trigger this function. Feed it resourceId.
function toggleResource(resourceId) {
var index = visibleResourceIds.indexOf(resourceId);
if (index !== -1) {
visibleResourceIds.splice(index, 1);
} else {
visibleResourceIds.push(resourceId);
}
$('#calendar').fullCalendar('refetchResources');
}
$('#calendar').fullCalendar({
defaultView: 'agendaDay',
resources: function(callback) {
// Filter resources by whether their id is in visibleResourceIds.
var filteredResources = [];
filteredResources = resourceData.filter(function(x) {
return visibleResourceIds.indexOf(x.id) !== -1;
});
callback(filteredResources);
}
});
I had the same challenge. Instead of a dropdown, I use checkboxes, but the workings will be the same.
My resources are stored in a variable, when I uncheck a box, the resource is removed and the resource's object is added to another array with the resourceId as key, and the index added to the object to restore the object in the same column as it originally was. When re-checking the box, the object is added to the resources array and the resources refetched.
/* retrieve the resources from the server */
var planningResources;
var removedResource = [];
$.ajax({
url: '/planning/resources/',
method: 'get',
success: function (response) {
planningResources = response;
showCalendar();
}
, error: function () {
if (typeof console == "object") {
console.log(xhr.status + "," + xhr.responseText + "," + textStatus + "," + error);
}
}
});
/* create the calendar */
showCalendar = function () {
$('#calendar').fullCalendar({
...
});
}
/* checkbox on click */
$('.resource').click(function() {
var resourceId = $(this).val();
var hideResource = !$(this)[0].checked;
$('.status:checkbox:checked').each(function () {
});
if(hideResource) {
$.each(planningResources, function(index, value){
if( value && value.id == resourceId ) {
value.ndx = index;
removedResource[resourceId] = value;
planningResources.splice(index,1);
return false;
}
});
$('#planningoverview').fullCalendar(
'removeResource',
resourceId
);
}
else {
planningResources.splice(removedResource[resourceId].ndx, 0, removedResource[resourceId]);
$('#planningoverview').fullCalendar('refetchResources');
}
});
showCalendar();
It probably doesn't get first price in a beauty contest, but it works for me ...
Cheers
You can use the resourceColumns option for this. In the column objects you can set the width property to a number of pixels or a percentage. If you pass a function here you can easily handle the width property someplace else. Your hide/show function can then set the width to 0 to hide the column. After that you can trigger reinitView to update the view: $('#calendar').fullCalendar("reinitView");

How to make TagsInput to work with both auto complete & free text

UPDATE
This issue is already discussed in github here
I am using tagsinput with typeahead in bootstrap 3. The problem which I am experiencing is with the value in case if user selects the existing tag. Display text shows it right but .val() returns its actual object. Below is the code
$('#tags').tagsinput({
//itemValue: 'value',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
// return process(tags);
//});
}
}
//freeElementSelector: "#freeTexts"
});
The problem with above code is that it results as below while fetching tags from web method
This happens when user select the existing tag. New tags no issues. I tried setting itemValue & itemText of tagsinput but not worked. Hence I decided a work-around of this problem. Since I could able get the json string as ['IRDAI", Object], if can somehow parse these object & get the actual tag value then I get the expected result of the code I am looking at.
Below is what it appears in tags input as [object Object] for text selected by user from auto populated drop down
[![enter imt
If I i specify TagId & TagValue to itemValue & itemText as below code
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// //map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
//});
// return process(tags);
}
}
//freeElementSelector: "#freeTexts"
});
Then the result is displaying as below when below code is executed
var arr = junit.Tags.split(',');
for (var i = 0; i < arr.length; i++) {
$('#tags').tagsinput('add', arr[i]);
}
Given your example JSON response from your data source:
[
{"TagId":"1", "TagValue":"eSign"},
{"TagId":"2", "TagValue":"eInsurance Account"}
]
You'll need to tell tagsinput how to map the attributes from your response objects using itemValue and itemText in your tagsinput config object. It looks like you may have started down that path, but didn't reach the conclusion, which should look something like:
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query);
}
}
});
Be sure to checkout the tagsinput examples.
This may not be the clean solution but I got around this issue through below parsing method. Hope this helps someone.
var items = $('#tags').tagsinput("items");
var tags = '';
for(i = 0; i < items.length; i++)
{
if(JSON.stringify(items[i]).indexOf('{') >= 0) {
tags += items[i].TagValue;
tags += ',';
} else {
tags += items[i];
tags += ',';
}
}

Reactive cursor without updating UI for added record

I am trying to make a newsfeed similar to twitter, where new records are not added to the UI (a button appears with new records count), but updates, change reactively the UI.
I have a collection called NewsItems and I a use a basic reactive cursor (NewsItems.find({})) for my feed. UI is a Blaze template with a each loop.
Subscription is done on a route level (iron router).
Any idea how to implement this kind of behavior using meteor reactivity ?
Thanks,
The trick is to have one more attribute on the NewsItem Collection Say show which is a boolean. NewsItem should have default value of show as false
The Each Loop Should display only Feeds with show == true and button should show the count of all the items with show == false
On Button click update all the elements in the Collection with show == false to show = true
this will make sure that all your feeds are shown .
As and when a new feed comes the Button count will also increase reactively .
Hope this Helps
The idea is to update the local collection (yourCollectionArticles._collection): all articles are {show: false} by default except the first data list (in order not to have a white page).
You detect first collection load using :
Meteor.subscribe("articles", {
onReady: function () {
articlesReady = true;
}
});
Then you observe new added data using
newsItems = NewsItems.find({})
newsItems.observeChanges({
addedBefore: (id, article, before)=> {
if (articlesReady) {
article.show = false;
NewsItems._collection.update({_id: id}, article);
}
else {
article.show = true;
NewsItems._collection.update({_id: id}, article);
}
}
});
Here is a working example: https://gist.github.com/mounibec/9bc90953eb9f3e04a2b3.
Finally I managed it using a session variable for the current date /time:
Template.newsFeed.onCreated(function () {
var tpl = this;
tpl.loaded = new ReactiveVar(0);
tpl.limit = new ReactiveVar(30);
Session.set('newsfeedTime', new Date());
tpl.autorun(function () {
var limit = tpl.limit.get();
var time = Session.get('newsfeedTime');
var subscription = tpl.subscribe('lazyload-newsfeed', time, limit);
var subscriptionCount = tpl.subscribe('news-count', time);
if (subscription.ready()) {
tpl.loaded.set(limit);
}
});
tpl.news = function() {
return NewsItems.find({creationTime: {$lt: Session.get('newsfeedTime')}},
{sort: {relevancy: -1 }},
{limit: tpl.loaded.get()});
},
tpl.countRecent = function() {
return Counts.get('recentCount');
},
tpl.displayCount = function() {
return Counts.get('displayCount');
}
});
Template.newsFeed.events({
'click .load-new': function (evt, tpl) {
evt.preventDefault();
var time = new Date();
var limit = tpl.limit.get();
var countNewsToAdd = tpl.countRecent();
limit += countNewsToAdd;
tpl.limit.set(limit);
Session.set('newsfeedTime', new Date());
}
});

Filter and search using Textbox

This question is a follow-up of My Previous Question on Filtering DropdownList
I am adding an extra feature and for that I want to Filter values using a textbox. Here is the code for filter
var filterAndLimitResults = function (cursor) {
if (!cursor) {
return [];
}
var raw = cursor.fetch();
var currentSearchTitle = searchTitle.get();
if(!(!currentSearchTitle || currentSearchTitle == "")) {
filtered = _.filter(filtered, function (item) {
return item.title === currentSearchTitle ;
console.log(item.title === currentSearchTitle);
});
}
var currentLimit =limit.get();
//enforce the limit
if (currentLimit ) {
filtered = _.first(filtered, currentLimit );
}
return filtered;
};
and this is the keyup event i am doing on the search textbox
"keyup #search-title":function(e,t){
if(e.which === 27){
searchTitle.set("");
}
else {
var text = $(e.target.val);
searchTitle.set(text)
console.log(searchTitle.set(text));
}
}
With this i am able to return total collection objects on each keypress in the console but it is not filtering the values in the listing and it vanishies all the listings from the UI. Please help
You are almost right. Your keyUp event is ok but you could also avoid to use jQuery like this:
"keyup .textInput": function(e, t) {
var searchString = e.currentTarget.value;
switch (e.which) {
case 27:
e.currentTarget.value = "";
searchTitle.set("");
break;
default:
searchTitle.set(searchString);
}
}
Note that I use a switch in case you would want to add shortcuts for specific searches, like cmd+maj+c to search only for cities (it might be a little overkill)
Concerning the search function, I assume you want to search among the titles of your items, within the current dropdown filtering.
You then need to add an extra step to do this. You also need to set it before the other filters. See my example below, you insert it after var filtered = [];:
var filtered = [];
var currentSearchTitle = searchTitle.get();
if(!currentSearchTitle || currentSearchTitle == "") {
filtered = raw;
} else {
currentSearchTitle = currentSearchTitle .replace(".", "\\.");//for regex
var regEx = new RegExp(currentSearchTitle , "i");
filtered = _.filter(raw, function(item) {
return (item && item.title && item.title.match(regEx));
});
}
// your other filtering tasks
return filtered;
Also, take the time to understand what the code does, you must not just copy paste it.
This code is strongly inspired by the work of meteorkitchen author, #Perak. I adapted it but I have not tested it "as-is".

Resources