app/dxa/dxa-entity/views/dist-office-search-views/dist-office-search/dist-office-search.component.ts
selector | div[dist-office-search] |
styleUrls | ./dist-office-search.component.scss |
templateUrl | ./dist-office-search.component.html |
constructor(distOfficeService: DistOfficeSearchService, mapsAPILoader: MapsAPILoader, changeDetectorRef: ChangeDetectorRef, activatedRoute: ActivatedRoute, router: Router, labelService: LabelService, ngZone: NgZone, util: UtilService, document)
|
||||||||||||||||||||||||||||||
Parameters :
|
entity | |
Type : any
|
|
class |
Public boundChanged | ||||||
boundChanged(latLngBounds: any)
|
||||||
Called whenever the map bounds are updated.
Parameters :
Returns :
void
|
buildQuery | ||||
buildQuery(filters)
|
||||
Builds search query from given filters
Parameters :
Returns :
string
|
Public checkInput |
checkInput()
|
Returns :
void
|
Public clearSelectedDistOffice |
clearSelectedDistOffice()
|
Returns :
void
|
Public clickedDistributor | ||||||
clickedDistributor(id: string)
|
||||||
Parameters :
Returns :
void
|
Public fetchProductandDistributorTaxonomy |
fetchProductandDistributorTaxonomy()
|
Get dropdown list of distributors and product categories.
Returns :
void
|
findCategory |
findCategory()
|
Search for Distributor offices given selected filter
Returns :
void
|
findDistributorCategory | ||||
findDistributorCategory(distributorCategories)
|
||||
Parameters :
Returns :
void
|
findProductCategory | ||||
findProductCategory(productCategories)
|
||||
Parameters :
Returns :
void
|
Public getIcon | ||||||
getIcon(selected, selectedDistOfficeId)
|
||||||
Setting the map-marker icon depending on which type of application it is.
Parameters :
Returns :
string
|
Public loadMapAPI | ||||
loadMapAPI(searchElement)
|
||||
Load Maps API and add listeners for auto completion.
Parameters :
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
Public onChangeMapPosition | ||||||
onChangeMapPosition(place: google.maps.places.PlaceResult)
|
||||||
This method is being called when the user searches in the search-box or a query is passed in the URL, it changes the maps boundingbox to show the given place's viewport.
Parameters :
Returns :
void
|
removeFilter | ||||
removeFilter(filter)
|
||||
Remove one of applied filters and make a new search.
Parameters :
Returns :
void
|
Public updateDistOffices |
updateDistOffices()
|
This is the method that will call the distOfficeService to get the avaliable locations. It will start with checking which application is set in the component, and then call the getDistOffices - method. It takes two arguments, a boundingBox and the type.
Returns :
void
|
Public updateMaxZoom | ||||
updateMaxZoom(zoomValue)
|
||||
Parameters :
Returns :
void
|
Public updateZIndexOnClick | ||||
updateZIndexOnClick(id)
|
||||
Set selected marker to be shown on top
Parameters :
Returns :
number
|
Public activatedRoute |
Type : ActivatedRoute
|
Public autocomplete |
Type : google.maps.places.Autocomplete
|
boundsFromSearchField |
Type : google.maps.LatLngBoundsLiteral | google.maps.LatLngBounds | boolean
|
Default value : false
|
Public changeDetectorRef |
Type : ChangeDetectorRef
|
checkedDistributors |
Type : []
|
Default value : []
|
checkedFilters |
Type : []
|
Default value : []
|
checkedProducts |
Type : []
|
Default value : []
|
Public currentBBox |
Type : BoundingBox
|
distibutorCategoryLabel |
Type : string
|
Public distOfficeList |
Type : []
|
Default value : []
|
Public distOfficeService |
Type : DistOfficeSearchService
|
distributorCategoryDropdownList |
Type : []
|
Default value : []
|
distributorForm |
Public distributorLoading |
Type : boolean
|
Public document |
Decorators :
@Inject(DOCUMENT)
|
findLabel |
Type : string
|
Public iconUrl |
Type : string[]
|
Default value : [
'/v2/assets/img/skf-distributor-marker.svg',
'/v2/assets/img/skf-offices-marker.svg']
|
Public iconUrlSelected |
Type : string[]
|
Default value : [
'/v2/assets/img/skf-distributor-selected.svg',
'/v2/assets/img/skf-offices-selected.svg'
]
|
isDistributor |
Type : boolean
|
latitude |
Type : number
|
listTitleLabel |
Type : string
|
locationlabel |
Type : string
|
locationPlaceholderLabel |
Type : string
|
Labels |
longitude |
Type : number
|
mapConfig |
Type : MapConfig
|
mapElement |
Type : AgmMap
|
Decorators :
@ViewChild('map')
|
Public mapsAPILoader |
Type : MapsAPILoader
|
productCategoryDropdownList |
Type : []
|
Default value : []
|
productCategoryLabel |
Type : string
|
productsForm |
searchControl |
Type : FormControl
|
Default value : new FormControl()
|
searchElementRef |
Type : ElementRef
|
Decorators :
@ViewChild('search')
|
Public selectedDistCategories |
Type : []
|
Default value : []
|
selectedDistOffice |
Type : SelectedDistOffice
|
subscriptions |
Type : []
|
Default value : []
|
zoom |
Type : number
|
import { Component, OnInit, Input, ChangeDetectorRef, HostBinding, Inject, OnDestroy, ViewChild, ElementRef, NgZone} from '@angular/core';
import { DistOfficeSearchService } from '../dist-office-search.service';
import { BoundingBox, MapConfig, SelectedDistOffice } from '../dist-office-search.model';
import { MapsAPILoader, AgmMap } from '@agm/core';
import { LabelService } from 'src/app/core/services/label-service/label-service.service';
import { ActivatedRoute, Router } from '@angular/router';
import { DOCUMENT } from '@angular/common';
import { FormControl } from '@angular/forms';
import { UtilService } from 'src/app/core/services/util-service/util.service';
@Component({
selector: 'div[dist-office-search]',
templateUrl: './dist-office-search.component.html',
styleUrls: ['./dist-office-search.component.scss'],
})
export class DistOfficeSearchComponent implements OnInit, OnDestroy {
@Input() entity: any;
public currentBBox: BoundingBox;
public distributorLoading: boolean;
public distOfficeList = [];
public autocomplete: google.maps.places.Autocomplete;
boundsFromSearchField: google.maps.LatLngBoundsLiteral | google.maps.LatLngBounds | boolean = false;
public selectedDistCategories = [];
public iconUrl: string[] = [
'/v2/assets/img/skf-distributor-marker.svg',
'/v2/assets/img/skf-offices-marker.svg'];
public iconUrlSelected: string[] = [
'/v2/assets/img/skf-distributor-selected.svg',
'/v2/assets/img/skf-offices-selected.svg'
];
mapConfig: MapConfig;
selectedDistOffice: SelectedDistOffice;
/** Labels */
locationPlaceholderLabel: string;
productCategoryLabel: string;
distibutorCategoryLabel: string;
locationlabel: string;
listTitleLabel: string;
findLabel: string;
productCategoryDropdownList = [];
distributorCategoryDropdownList = [];
subscriptions = [];
isDistributor: boolean;
checkedDistributors = [];
checkedProducts = [];
checkedFilters = [];
latitude: number;
longitude: number;
zoom: number;
productsForm;
distributorForm;
searchControl: FormControl = new FormControl();
@ViewChild('search') searchElementRef: ElementRef;
@ViewChild('map') mapElement: AgmMap;
@HostBinding('class') get class() { return 'col-md-12 space-between'; }
constructor(
public distOfficeService: DistOfficeSearchService,
public mapsAPILoader: MapsAPILoader,
public changeDetectorRef: ChangeDetectorRef,
public activatedRoute: ActivatedRoute,
private router: Router,
private labelService: LabelService,
private ngZone: NgZone,
private util: UtilService,
@Inject(DOCUMENT) public document
) { }
ngOnInit(): void {
this.labelService.getLabel('location').then(label => this.locationlabel = label);
this.labelService.getLabel('locationPlaceholder').then(label => this.locationPlaceholderLabel = label);
this.labelService.getLabel('productCategoryDistOffice').then(label => this.productCategoryLabel = label);
this.labelService.getLabel('distibutorCategory').then(label => this.distibutorCategoryLabel = label);
this.labelService.getLabel('find').then(label => this.findLabel = label);
const startCoordinates = this.entity.StartCoordinates.split(',');
this.mapConfig = {
startLat: Number(startCoordinates[0]),
startLng: Number(startCoordinates[1]),
lat: Number(startCoordinates[0]),
lng: Number(startCoordinates[1]),
startZoom: Number(this.entity.ZoomFactor),
maxZoom: Number(this.entity.ZoomFactor),
appType: this.entity.TypeOfApplication
};
this.clearSelectedDistOffice();
this.isDistributor = this.util.extract(this.entity, 'TypeOfApplication') === 'Distributor search';
this.loadMapAPI(this.searchElementRef.nativeElement);
this.listTitleLabel = this.isDistributor ? 'SKF Distributors' : 'SKF Offices';
this.fetchProductandDistributorTaxonomy();
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
/** Get dropdown list of distributors and product categories. */
public fetchProductandDistributorTaxonomy(): void {
this.distOfficeService.distributorTaxonomyService('Product').toPromise().then(res => {
this.productCategoryDropdownList = res;
});
this.distOfficeService.distributorTaxonomyService('Distributor').toPromise().then(res => {
this.distributorCategoryDropdownList = res;
});
}
/** Load Maps API and add listeners for auto completion. */
public loadMapAPI(searchElement): void {
this.mapsAPILoader.load().then(() => {
this.autocomplete = new google.maps.places.Autocomplete(searchElement, {
types: ['geocode', 'establishment']
});
this.autocomplete.addListener('place_changed', () => {
this.ngZone.run(() => {
this.onChangeMapPosition(this.autocomplete.getPlace());
});
});
/** Get search query from url if available and pass into location box, and perform a place search */
this.subscriptions.push(this.activatedRoute.queryParams.subscribe(params => {
if (params.search) {
const value = decodeURIComponent(params.search);
searchElement.value = value;
/** PlacesService requires a HTML element for some reason, so we supply a dummy one */
const service = new google.maps.places.PlacesService(this.document.createElement('div'));
/** Fetch the first result from a place query and pass it into onChangeMapPosition */
service.findPlaceFromQuery({ query: value, fields: [ 'geometry' ] }, function(results) {
if (results && results.length > 0) {
this.onChangeMapPosition(results[0]);
}
}.bind(this));
}
}));
});
}
/** This method is being called when the user searches in the search-box or a query is
passed in the URL, it changes the maps boundingbox to show the given place's viewport. */
public onChangeMapPosition(place: google.maps.places.PlaceResult) {
if (!place || !place.geometry) {
return;
}
if (place.geometry.viewport) {
this.boundsFromSearchField = place.geometry.viewport;
} else if (place.geometry.location) {
this.mapConfig.lat = place.geometry.location.lat();
this.mapConfig.lng = place.geometry.location.lng();
}
this.clearSelectedDistOffice();
}
public clearSelectedDistOffice() {
this.selectedDistOffice = {
id: null
};
}
public clickedDistributor(id: string) {
const distributor = this.distOfficeList.find(dist => dist.id === id);
/** If zoomed in on a distributor, unselect and zoom out. */
if ( id === this.selectedDistOffice.id) {
this.clearSelectedDistOffice();
this.mapConfig.maxZoom = this.mapConfig.startZoom;
} else {
let newLat = Number(distributor.map_latitude);
let newLng = Number(distributor.map_longitude);
/** If going back to same location after moving the map manually, the map will zoom on but not change center, causing a bug.
* Changing the location by a small number will make it update correctly.
*/
if (this.mapConfig.lat === distributor.map_latitude && this.mapConfig.lng === distributor.map_longitude) {
newLat += 0.00001;
newLng += 0.00001;
}
this.mapConfig.lat = newLat;
this.mapConfig.lng = newLng;
this.selectedDistOffice = distributor;
this.selectedDistCategories = [{ 'distributor_category': distributor.distributor_category, 'product_category': distributor.product_category}];
this.mapConfig.maxZoom = 16;
}
}
/** Called whenever the map bounds are updated. */
public boundChanged(latLngBounds: any) {
this.currentBBox = {
sw: {
lat: latLngBounds.getSouthWest().lat(),
lng: latLngBounds.getSouthWest().lng()
},
ne: {
lat: latLngBounds.getNorthEast().lat(),
lng: latLngBounds.getNorthEast().lng()
}
};
}
/** This is the method that will call the distOfficeService to get the avaliable locations.
* It will start with checking which application is set in the component, and then call the getDistOffices - method.
* It takes two arguments, a boundingBox and the type. */
public updateDistOffices(): void {
const type = this.isDistributor ? 'distributors' : 'offices';
this.distributorLoading = true;
this.distOfficeService.getDistOffices(this.currentBBox, type, this.buildQuery(this.checkedFilters))
.toPromise().then(distOffice => {
/** Sorts the list in alphabetical order */
this.distOfficeList = distOffice.docs.sort(( a, b) => {
if (a.name < b.name) {return -1; }
if (a.name > b.name) {return 1; }
return 0;
});
let duplicate;
if (this.selectedDistOffice.id) {
/** Sorts so the selected one will be on top of the list. */
this.distOfficeList = this.distOfficeList.sort (( a ) => {
if (a.id === this.selectedDistOffice.id) { return -1; }
return 0;
});
/** Check if the selected distributor/office excists in the list. (Might loose it due to the random fetch from the API) */
duplicate = this.distOfficeList.find(dist => dist.id === this.selectedDistOffice.id);
}
if (!duplicate && this.selectedDistOffice.id) {
this.distOfficeList.pop();
/** This is needed due to otherwise the catagories will be blank efter adding the selectedDistOffice to the list. */
this.selectedDistOffice.distributor_category = this.selectedDistCategories[0].distributor_category;
this.selectedDistOffice.product_category = this.selectedDistCategories[0].product_category;
this.distOfficeList.unshift(this.selectedDistOffice);
if (this.isDistributor) {
this.distOfficeService.getTaxonomy(this.distOfficeList, this.productCategoryDropdownList, this.distributorCategoryDropdownList);
}
}
else if (this.isDistributor && this.distOfficeList && duplicate) {
this.distOfficeService.getTaxonomy(this.distOfficeList, this.productCategoryDropdownList, this.distributorCategoryDropdownList);
}
this.distributorLoading = false;
}, err => {
this.distributorLoading = false;
}).catch((error) => {
console.error(error);
});
}
/** Setting the map-marker icon depending on which type of application it is. */
public getIcon(selected, selectedDistOfficeId): string {
if (this.mapConfig.appType === 'Distributor search') {
return (selected === selectedDistOfficeId) ? this.iconUrlSelected[0] : this.iconUrl[0];
} else if (this.mapConfig.appType === 'Office search') {
return (selected === selectedDistOfficeId) ? this.iconUrlSelected[1] : this.iconUrl[1];
}
}
findProductCategory(productCategories): void {
this.productsForm = productCategories;
/** Find selected filters */
const filters = Object.keys(productCategories.value);
const selectedFilters = filters.filter(filter => productCategories.value[filter]);
this.checkedProducts = this.productCategoryDropdownList.filter((data) => selectedFilters.includes(data.Value));
this.checkedProducts = this.checkedProducts.map(product => {
product.type = 'product';
return product;
});
this.findCategory();
}
findDistributorCategory(distributorCategories): void {
this.distributorForm = distributorCategories;
/** Find selected filters */
const filters = Object.keys(distributorCategories.value);
const selectedFilters = filters.filter(filter => distributorCategories.value[filter]);
this.checkedDistributors = this.distributorCategoryDropdownList.filter((data) => selectedFilters.includes(data.Value));
this.checkedDistributors = this.checkedDistributors.map(dist => {
dist.type = 'distributor';
return dist;
});
this.findCategory();
}
/** Search for Distributor offices given selected filter */
findCategory(): void {
this.checkedFilters = [...this.checkedProducts, ...this.checkedDistributors];
const query = this.buildQuery(this.checkedFilters);
const type = this.isDistributor ? 'distributors' : 'offices';
this.distOfficeService.getDistOffices(this.currentBBox, type, query).toPromise().then(res => {
const distOfficeList = res.docs;
this.distOfficeService.getTaxonomy(distOfficeList, this.productCategoryDropdownList, this.distributorCategoryDropdownList);
this.distOfficeList = distOfficeList;
});
}
/** Remove one of applied filters and make a new search. */
removeFilter(filter) {
if (filter.type === 'product') {
this.checkedProducts = this.checkedProducts.filter(product => product.Value !== filter.Value);
this.productsForm.controls[filter.Value].setValue(false);
} else if (filter.type === 'distributor') {
this.checkedDistributors = this.checkedDistributors.filter(dist => dist.Value !== filter.Value);
this.distributorForm.controls[filter.Value].setValue(false);
}
this.findCategory();
}
/** Builds search query from given filters */
buildQuery(filters): string {
let query = '';
filters.forEach(filter => {
if (filter.type === 'product') {
query += `&product_category=${filter.Value}`;
} else if (filter.type === 'distributor') {
query += `&distributor_category=${filter.Value}`;
}
});
return query;
}
/** Set selected marker to be shown on top */
public updateZIndexOnClick(id): number {
if (this.selectedDistOffice.id === id) {
return 999;
} else {
return 1;
}
}
public checkInput() {
/** Remove query params when doing new search, if set from widget. */
if (this.activatedRoute.snapshot.queryParams['search']) {
this.router.navigate([], {
queryParams: {},
replaceUrl: true
});
}
}
public updateMaxZoom(zoomValue) {
this.mapConfig.maxZoom = zoomValue;
}
}
<div id="component-container">
<div class="row">
<div class="col-lg-4 col-sm-12 filter-col">
<p>{{locationlabel}}</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<img src="/v2/assets/img/map-pointer.png" alt="map-pointer">
</span>
</div>
<input select-text-on-focus #search [placeholder]="locationPlaceholderLabel" autocorrect="off" autocapitalize="off"
spellcheck="off" type="search" class="form-control search-box-query" (keyup.enter)="checkInput()"
[formControl]="searchControl">
</div>
</div>
<div class="col-lg-4 col-sm-12 filter-col" *ngIf="productCategoryDropdownList.length > 0 && isDistributor">
<p>{{productCategoryLabel}}</p>
<div multi-select-drop-down
[dataList]="productCategoryDropdownList"
[buttonLabel]="findLabel"
(findQuery)="findProductCategory($event)">
</div>
</div>
<div class="col-lg-4 col-sm-12 filter-col" *ngIf="distributorCategoryDropdownList.length > 0 && isDistributor">
<p>{{distibutorCategoryLabel}}</p>
<div multi-select-drop-down
[dataList]="distributorCategoryDropdownList"
[buttonLabel]="findLabel"
(findQuery)="findDistributorCategory($event)">
</div>
</div>
</div>
<div class="line"></div>
<div class="used-filters">
<ng-container *ngFor="let filter of checkedFilters">
<span (click)="removeFilter(filter)">
{{filter.Key | uppercase}}
<span class="badge badge-pill"><i class="icon-nav-close"></i></span>
</span>
</ng-container>
</div>
<h3>{{listTitleLabel}}</h3>
<div class="row dist-office-list">
<div class="col-lg-5 mb-4">
<div distributor-search-list [distOfficeList]="distOfficeList" [distributorLoading]="distributorLoading"
[type]="mapConfig.appType" [selectedDistributor]="selectedDistOffice" (select)="clickedDistributor($event)">
</div>
</div>
<div class="col-lg-7 order-first order-md-2">
<div id="map-container">
<agm-map #map id="map" [zoom]="mapConfig.maxZoom" [latitude]="mapConfig.lat"
[longitude]="mapConfig.lng" [fitBounds]="boundsFromSearchField"
(boundsChange)="boundChanged($event)" (idle)="updateDistOffices()" (zoomChange)="updateMaxZoom($event)">
<agm-marker *ngFor="let distributor of distOfficeList"
[latitude]="distributor.map_latitude" [longitude]="distributor.map_longitude" [zIndex]="updateZIndexOnClick(distributor.id)"
[iconUrl]="{ url: getIcon(distributor.id, selectedDistOffice.id),scaledSize: {width: 40,height: 40}}"
[agmFitBounds]="true" (markerClick)="clickedDistributor(distributor.id)">
</agm-marker>
</agm-map>
</div>
</div>
</div>
</div>
./dist-office-search.component.scss
@import "src/app/styles/helpers";
.line {
width: 100%;
margin-top: calc-rem(10);
height: 3px;
border-bottom: solid 1px $grey;
@include media-breakpoint-down(md) {
margin-bottom: calc-rem(20);
}
}
.sebm-google-map-container {
height: 500px;
}
p {
@include font-size(32);
}
#component-container {
height: 100%;
margin-bottom: calc-rem(32);
margin-top: calc-rem(32);
p {
font-size: calc-rem(24);
}
.filter-col {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: calc-rem(20);
.input-group-text {
background-color: #F6F6F6; // Color of image background.
padding: 0 calc-rem(8);
border: 1px solid $light-slate;
img {
width: calc-rem(30);
height: calc-rem(24);
}
}
.search-box-query {
height: calc-rem(41);
padding: calc-rem(8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: 1px solid $light-slate;
border-left: none;
&:focus {
box-shadow: none;
}
}
}
.used-filters {
display: none;
flex-direction: row;
flex-wrap: wrap;
padding: calc-rem(10) 0;
min-height: calc-rem(50);
@include media-breakpoint-up(md) {
display: flex;
}
span {
@include font-size(14);
margin-right: calc-rem(10);
margin-bottom: calc-rem(5);
cursor: pointer;
.badge {
background-color: $slate;
border-radius: 10em;
padding: 0;
margin-left: calc-rem(3);
}
i {
color: $dark_blue;
margin: 0;
}
}
}
}
#map-container {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: calc-rem(32);
}
.dist-office-list{
div{
padding:calc-rem(8);
}
}
@include media-breakpoint-down(md) {
.sebm-google-map-container {
height: 270px;
}
}