import STQ_LocationSearch from '../../resources/js/classes/LocationSearch';
class FindFirm {
/**
* Create and initialise objects of this class
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor
* @param {object} block
*/
constructor() {
this.blocks = document.querySelectorAll('.block-find-firm');
this.firms = this.blocks[0].querySelectorAll('.block-find-firm__map-location');
this.country = false;
this.region = false;
this.setZoom = 1.25;
this.setCenter = [30.562, 31.562];
this.map = null;
this.allFirms = null;
this.allFirmMapDetails = null;
if (window.innerWidth < 768) {
this.mobileShowFitlers();
}
this.init();
}
/**
* Example function to run class logic
* Can access `this.block`
*/
init() {
this.filters();
this.setupFirmClick();
this.initMap(this.blocks[0]);
new STQ_LocationSearch();
}
initMap(block) {
if (!mapboxgl) {
console.error('Mapbox GL not loaded');
return;
}
const mapContainer = block.querySelector('.block-find-firm__map');
// Get the coords for the map if country set
const country_coords = mapContainer.dataset.coords;
let country_coordsArray = [];
if (country_coords) {
country_coordsArray = JSON.parse(country_coords);
}
// Add loading state
mapContainer.classList.add('is-loading');
// Helper function to create canvas with willReadFrequently
const createImageCanvas = (img) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
return canvas;
};
// Modify the preloadImages function
const preloadImages = () => {
return Promise.all([
new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = createImageCanvas(img);
resolve(canvas);
};
img.onerror = reject;
img.src = window.location.origin + '/wp-content/themes/moore-global/assets/icons/map-pin.png';
}),
new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = createImageCanvas(img);
resolve(canvas);
};
img.onerror = reject;
img.src = window.location.origin + '/wp-content/themes/moore-global/assets/icons/map-pin-selected.png';
})
]);
};
// Initialize map with preloaded data
preloadImages().then(([defaultPinCanvas, selectedPinCanvas]) => {
mapboxgl.accessToken = 'pk.eyJ1Ijoic3RyYXRlZ2lxIiwiYSI6ImNqc2trb252NzEyZjYzenBvYTRhbmdyNDEifQ.la_5DQo_qJaALM-bp_sc2w';
this.map = new mapboxgl.Map({
container: mapContainer,
style: 'mapbox://styles/strategiq/cm6qi73pg010a01r5g5551c8i',
center: this.setCenter,
zoom: this.setZoom,
willReadFrequently: true,
cooperativeGestures: true,
preloadImages: true
});
// Add this after map initialization
const canvas = mapContainer.querySelector('canvas');
if (canvas) {
canvas.getContext('2d', { willReadFrequently: true });
}
// Rest of your map initialization code...
this.map.on('load', () => {
// Add the images using the canvas instead of direct image loading
if (!this.map.hasImage('map-pin')) {
this.map.addImage('map-pin', defaultPinCanvas.getContext('2d').getImageData(
0, 0,
defaultPinCanvas.width,
defaultPinCanvas.height
));
}
if (!this.map.hasImage('map-pin-selected')) {
this.map.addImage('map-pin-selected', selectedPinCanvas.getContext('2d').getImageData(
0, 0,
selectedPinCanvas.width,
selectedPinCanvas.height
));
}
// Add source and layers...
this.map.addSource('firms', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
},
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50,
});
// Add a layer for the clusters with simpler style
this.map.addLayer({
id: 'clusters',
type: 'circle',
source: 'firms',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#00AEEF',
'circle-radius': 20
}
});
// Add the cluster count layer
this.map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'firms',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['Montserrat Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
'text-allow-overlap': true
},
paint: {
'text-color': '#000000'
}
});
// Make sure these layers are rendered ABOVE all other layers
this.map.moveLayer('clusters');
this.map.moveLayer('cluster-count');
// Add single layer for unclustered points
this.map.addLayer({
'id': 'unclustered-point',
'type': 'symbol',
'source': 'firms',
'filter': ['!', ['has', 'point_count']],
'layout': {
'icon-image': [
'case',
['get', 'selected'],
'map-pin-selected',
'map-pin'
],
'icon-size': 1,
'icon-allow-overlap': true
}
});
// Update the click handler for unclustered points
this.map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const description = e.features[0].properties.description;
const clickedFirmId = e.features[0].properties.firmId;
// Reset all points
const features = this.map.getSource('firms')._data.features.map(feature => ({
...feature,
properties: {
...feature.properties,
selected: feature.properties.firmId === clickedFirmId
}
}));
this.map.getSource('firms').setData({
type: 'FeatureCollection',
features: features
});
this.showPopup(coordinates, description);
});
// Update the click-away handler
this.map.on('click', (e) => {
const features = this.map.queryRenderedFeatures(e.point, { layers: ['unclustered-point'] });
if (!features.length) {
// Reset all points to unselected
const features = this.map.getSource('firms')._data.features.map(feature => ({
...feature,
properties: {
...feature.properties,
selected: false
}
}));
this.map.getSource('firms').setData({
type: 'FeatureCollection',
features: features
});
}
});
// Add zoom and rotation controls to the map.
this.map.addControl(new mapboxgl.NavigationControl());
// Make sure clusters are always on top
this.map.on('idle', () => {
this.map.moveLayer('clusters');
this.map.moveLayer('cluster-count');
});
// Inside the map.on('load') callback, after adding the cluster layers:
this.map.on('click', 'clusters', (e) => {
const features = this.map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
const clusterId = features[0].properties.cluster_id;
this.map.getSource('firms').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
this.map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom,
duration: 500,
essential: true
});
}
);
});
// Optional: Add cursor styling for better UX
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
// If we have boundary coordinates, fit the map to them after initialization
if (country_coords) {
this.map.fitBounds(country_coordsArray, {
padding: { top: 50, bottom: 50, left: 50, right: 50 },
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
}
// Remove loading state when map is ready
mapContainer.classList.remove('is-loading');
// Load firms after map is ready
this.loadAllFirms();
});
});
}
// Method to show popup
showPopup(coordinates, description) {
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(description)
.addTo(this.map);
}
goToMarker(firm) {
const lng = parseFloat(firm.dataset.lng);
const lat = parseFloat(firm.dataset.lat);
// Reset all points and select the clicked one
const features = this.map.getSource('firms')._data.features.map(feature => ({
...feature,
properties: {
...feature.properties,
selected: String(feature.properties.firmId) === String(firm.getAttribute('data-firm-id'))
}
}));
this.map.getSource('firms').setData({
type: 'FeatureCollection',
features: features
});
this.map.flyTo({
center: [lng, lat],
zoom: 14,
essential: true
});
const description = firm.querySelector('.block-find-firm__map-location-details').innerHTML;
this.showPopup([lng, lat], description);
}
setupFirmClick() {
// Get all firm elements, including newly loaded ones
this.firms = this.blocks[0].querySelectorAll('.block-find-firm__map-location');
this.firms.forEach(firm => {
// Remove any existing click handlers
firm.removeEventListener('click', this.handleFirmClick);
// Add new click handler
firm.addEventListener('click', () => this.goToMarker(firm));
// Ensure accessibility attributes are set
if (!firm.hasAttribute('role')) {
firm.setAttribute('tabindex', '0');
firm.setAttribute('role', 'option');
// Add keyboard event handlers for option role
firm.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.goToMarker(firm);
}
});
}
});
}
filters() {
const filters = this.blocks[0].querySelector('.block-find-firm__map-filters');
if (!filters) return;
const filterItems = filters.querySelectorAll('select');
const regionFilter = filters.querySelector('select[name="region"]');
const countryFilter = filters.querySelector('select[name="country"]');
filterItems.forEach(filter => {
filter.addEventListener('change', () => {
this.region = regionFilter ? regionFilter.value : false;
this.country = countryFilter ? countryFilter.value : false;
// Filter firms client-side
this.filterFirms();
if (filter.name === 'country' || filter.name === 'region') {
// Change heading depending on filter
let headingText = 'All Locations';
// If changing country
if (filter.name === 'country') {
if (filter.value) {
headingText = filter.selectedOptions[0].textContent;
} else {
// If country is set to empty, check if region has a value
if (regionFilter && regionFilter.value) {
headingText = regionFilter.selectedOptions[0].textContent;
}
}
}
// If changing region
else if (filter.name === 'region') {
const countrySelect = document.querySelector('select[name="country"]');
// If country has value, use it
if (countrySelect && countrySelect.value) {
headingText = countrySelect.selectedOptions[0].textContent;
}
// Otherwise use region if it has value
else if (filter.value) {
headingText = filter.selectedOptions[0].textContent;
}
}
document.querySelector('.block-find-firm__map-search-heading').innerHTML = headingText;
}
if (filter.name === 'country') {
// Check if region is selected when "All" is chosen for country
if (!filter.value || filter.value === '') {
if (this.region && regionFilter && regionFilter.value) {
const regionOption = regionFilter.querySelector(`option[value="${regionFilter.value}"]`);
if (regionOption && regionOption.dataset.coords) {
const regionData = JSON.parse(regionOption.dataset.coords);
const bounds = regionData.bounds;
// Check if we have custom center and zoom settings
if (regionData.center && regionData.zoom) {
// Use the custom center and zoom from the JSON data
this.map.flyTo({
center: regionData.center,
zoom: regionData.zoom,
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
} else {
// Fall back to standard fitBounds if center/zoom not specified
this.map.fitBounds(bounds, {
padding: { top: 100, bottom: 100, left: 100, right: 100 },
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
}
} else {
// Region selected but no coordinates - reset to default
this.map.setCenter(this.setCenter);
this.map.setZoom(this.setZoom);
}
} else {
// No region selected - reset to default
this.map.setCenter(this.setCenter);
this.map.setZoom(this.setZoom);
}
} else if (filter.selectedOptions[0].dataset.coords) {
// Fit Map bounds only if coordinates are available
const bounds = JSON.parse(filter.selectedOptions[0].dataset.coords);
this.map.fitBounds(bounds, {
padding: { top: 50, bottom: 50, left: 50, right: 50 },
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
}
// If country selected but no coordinates, do nothing - keep current map position
}
if (filter.name === 'region') {
const countryFilter = filters.querySelector('select[name="country"]');
this.resetFilter(countryFilter);
// Check if region is selected
if (this.region && filter.value) {
const allOptions = Array.from(countryFilter.options);
allOptions.forEach(option => {
if (option.value) {
option.style.display = option.dataset.region === this.region ? 'block' : 'none';
}
});
// Fit Map bounds
if (filter.selectedOptions[0].dataset.coords) {
const regionData = JSON.parse(filter.selectedOptions[0].dataset.coords);
const bounds = regionData.bounds;
// Check if we have custom center and zoom settings
if (regionData.center && regionData.zoom) {
// Use the custom center and zoom from the JSON data
this.map.flyTo({
center: regionData.center,
zoom: regionData.zoom,
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
} else {
// Fall back to standard fitBounds if center/zoom not specified
this.map.fitBounds(bounds, {
padding: { top: 100, bottom: 100, left: 100, right: 100 },
duration: 2000,
essential: true,
curve: 1.42,
easing: function (t) {
return t * (2 - t);
}
});
}
} else {
// Region selected but no coordinates - reset to default
this.map.setCenter(this.setCenter);
this.map.setZoom(this.setZoom);
}
} else {
// No region selected or region reset - reset map to default
this.map.setCenter(this.setCenter);
this.map.setZoom(this.setZoom);
}
}
});
});
}
filterFirms() {
// Get all firms from the DOM
const firms = this.blocks[0].querySelectorAll('.block-find-firm__map-location');
// Get selected country and region
const selectedCountry = this.country;
const selectedRegion = this.region;
// Filter map details based on country and region
const filteredMapDetails = this.allFirmMapDetails.filter(firm => {
const firmElement = document.querySelector(`[data-firm-id="${firm.firm_id}"]`);
if (!firmElement) return false;
// Check for country (prefer data-country, fallback to data-location)
const firmCountry = firmElement.dataset.country ? firmElement.dataset.country : firmElement.dataset.location;
const firmRegion = firmElement.dataset.region;
// Match country
const matchesCountry = !selectedCountry || firmCountry === selectedCountry;
// Match region
const matchesRegion = !selectedRegion || firmRegion === selectedRegion;
return matchesCountry && matchesRegion;
});
// Update map with filtered data
if (this.map) {
const geojson = {
type: 'FeatureCollection',
features: filteredMapDetails.map(firm => ({
type: 'Feature',
properties: {
description: document.querySelector(`[data-firm-id="${firm.firm_id}"]`) ?
document.querySelector(`[data-firm-id="${firm.firm_id}"] .block-find-firm__map-location-details`).innerHTML :
'No description available',
firmId: firm.firm_id,
selected: false
},
geometry: {
type: 'Point',
coordinates: firm.coords
}
}))
};
const source = this.map.getSource('firms');
if (source) {
source.setData(geojson);
}
}
// Update firms list
firms.forEach(firm => {
// Check for country (prefer data-country, fallback to data-location)
const firmCountry = firm.dataset.country ? firm.dataset.country : firm.dataset.location;
const firmRegion = firm.dataset.region;
// Match country
const matchesCountry = !selectedCountry || firmCountry === selectedCountry;
// Match region
const matchesRegion = !selectedRegion || firmRegion === selectedRegion;
if (matchesCountry && matchesRegion) {
firm.style.display = '';
} else {
firm.style.display = 'none';
}
});
console.log('Filter by', selectedCountry, selectedRegion);
// Reattach click handlers
this.setupFirmClick();
}
resetFirms() {
this.firms.forEach(firm => {
firm.style.display = 'none';
});
}
resetFilter(filter) {
const allOptions = Array.from(filter.options);
if (filter.name === 'country' && filter.value) {
this.country = false; // Reset country when region is cleared
filter.selectedIndex = 0; // Select the first option
}
// Reset the selected index for the current filter
filter.selectedIndex = 0; // Select the first option
allOptions.forEach(option => {
option.style.display = 'block';
});
}
mobileShowFitlers() {
const filtersHeading = this.blocks[0].querySelector('.block-find-firm__map-filters-heading');
if (!filtersHeading) return;
const filters = this.blocks[0].querySelector('.block-find-firm__map-filters');
filtersHeading.addEventListener('click', () => {
filters.classList.add('active');
});
const closeFilters = this.blocks[0].querySelector('.block-find-firm__map-filters-close');
if (!closeFilters) return;
closeFilters.addEventListener('click', () => {
filters.classList.remove('active');
});
}
loadAllFirms() {
const url = new URL('/wp-json/wp/v2/firms/map', window.location.origin);
fetch(url)
.then(response => response.json())
.then(data => {
// Store all firms and map details
this.allFirms = data.firms;
this.allFirmMapDetails = data.firm_map_details;
// Populate firms list first
const firmsList = this.blocks[0].querySelector('.block-find-firm__map-search-scroll ul');
if (firmsList) {
firmsList.innerHTML = data.firms.join('');
// Make firm items focusable and accessible
const firmItems = firmsList.querySelectorAll('.block-find-firm__map-location');
firmItems.forEach(item => {
item.setAttribute('tabindex', '0');
item.setAttribute('role', 'option');
// Add keyboard event handlers for option role
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.goToMarker(item);
}
});
});
// Reattach click handlers after loading firms
this.setupFirmClick();
}
// Update map with firm data AFTER firms are loaded to DOM
if (this.map) {
const geojson = {
type: 'FeatureCollection',
features: data.firm_map_details.map(firm => ({
type: 'Feature',
properties: {
description: document.querySelector(`[data-firm-id="${firm.firm_id}"]`) ?
document.querySelector(`[data-firm-id="${firm.firm_id}"] .block-find-firm__map-location-details`).innerHTML :
'No description available',
firmId: firm.firm_id,
selected: false
},
geometry: {
type: 'Point',
coordinates: firm.coords
}
}))
};
const source = this.map.getSource('firms');
if (source) {
source.setData(geojson);
}
}
});
}
}
//ON document load
document.addEventListener('DOMContentLoaded', () => {
new FindFirm();
});