File

app/dxa/dxa-entity/views/dist-office-search-views/dist-office-search/dist-office-search.component.ts

Implements

OnInit OnDestroy

Metadata

selector div[dist-office-search]
styleUrls ./dist-office-search.component.scss
templateUrl ./dist-office-search.component.html

Index

Properties
Methods
Inputs
HostBindings

Constructor

constructor(distOfficeService: DistOfficeSearchService, mapsAPILoader: MapsAPILoader, changeDetectorRef: ChangeDetectorRef, activatedRoute: ActivatedRoute, router: Router, labelService: LabelService, ngZone: NgZone, util: UtilService, document)
Parameters :
Name Type Optional
distOfficeService DistOfficeSearchService No
mapsAPILoader MapsAPILoader No
changeDetectorRef ChangeDetectorRef No
activatedRoute ActivatedRoute No
router Router No
labelService LabelService No
ngZone NgZone No
util UtilService No
document No

Inputs

entity
Type : any

HostBindings

class

Methods

Public boundChanged
boundChanged(latLngBounds: any)

Called whenever the map bounds are updated.

Parameters :
Name Type Optional
latLngBounds any No
Returns : void
buildQuery
buildQuery(filters)

Builds search query from given filters

Parameters :
Name Optional
filters No
Returns : string
Public checkInput
checkInput()
Returns : void
Public clearSelectedDistOffice
clearSelectedDistOffice()
Returns : void
Public clickedDistributor
clickedDistributor(id: string)
Parameters :
Name Type Optional
id string No
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 :
Name Optional
distributorCategories No
Returns : void
findProductCategory
findProductCategory(productCategories)
Parameters :
Name Optional
productCategories No
Returns : void
Public getIcon
getIcon(selected, selectedDistOfficeId)

Setting the map-marker icon depending on which type of application it is.

Parameters :
Name Optional
selected No
selectedDistOfficeId No
Returns : string
Public loadMapAPI
loadMapAPI(searchElement)

Load Maps API and add listeners for auto completion.

Parameters :
Name Optional
searchElement No
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 :
Name Type Optional
place google.maps.places.PlaceResult No
Returns : void
removeFilter
removeFilter(filter)

Remove one of applied filters and make a new search.

Parameters :
Name Optional
filter No
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 :
Name Optional
zoomValue No
Returns : void
Public updateZIndexOnClick
updateZIndexOnClick(id)

Set selected marker to be shown on top

Parameters :
Name Optional
id No
Returns : number

Properties

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;
    }
}

Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""