Webflow Tutorial
How to Add a MapBox Map to a Webflow Project - Complete Guide
Introduction
Are you looking to add a Mapbox map to a Webflow project? Integrating interactive maps into your website can significantly enhance user experience and engagement.
In this complete guide, I’ll walk you through the step-by-step process of incorporating Mapbox maps into your Webflow project.
We'll be using the Webflow CMS as well so that you can easily add new locations to the map.
From setting up your project to customizing your map’s appearance and adding geolocation features, this tutorial covers everything you need to know.
Transform your website with dynamic, visually appealing maps that captivate your audience and add a new dimension to your site's functionality.
Overview
This script integrates a Mapbox map into a Webflow project, displaying a list of locations on the map and in a sidebar.
Users can search for locations, and the map will zoom to the selected location, displaying a detailed map card and popup.
The script uses GeoJSON data to render location points on the map and provides geolocation functionality to find the user's current position.
Sorting Instructions
I mention this here to avoid any major errors.
In the JS, there is a section that will sort the sidebar list from A-Z. You can comment this out if you decide to sort using Webflow’s collection list sort feature.
However, and this is crucial, IF you use Webflow to sort, you have to sort both the sidebar list and the card list. If you sort one by city name A-Z and not the other, the correct data-attribute that correlates the two lists will not be added and there will be a mismatch.
CMS Setup
Unless you plan to hardcode the GeoJSON data as well as each card and the sidebar items - not recommended - you will need to setup a locations CMS collection. The JavaScript code is setup to use the locations CMS to display the points on the map, the sidebar items, as well as the card items.
At minimum, you will need the following fields for the Script to work properly. As well as these fields need to be connected to a collection list that is placed on the page.
- Name
- Featured Image
- City
- This script correlates everything based on the city name in the CMS. The locationID is based on this city name, which means the city name needs to be unique. It cannot be a duplicate. If you find it is being duplicated, you can use another selector that will not be duplicated as the locationID (see code base).
- Latitude
- Longitude
- Every other field is optional depending on your setup.
Dependencies
In order to render the map in a Webflow project, you’ll need to use Mapbox’s CDN. Here are the links to the Javascript files as well as to the CSS files.
Paste these in the head of the page on which you want the map to show up.
MapBox Core: Minimum needed for map functionality.
<!-- Mapbox Resources -->
<script src='https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.css' rel='stylesheet' />
Mapbox Geocoder: Plugin for search functionality. You’ll need this library if you want to use search or the location locator.
<!-- Geocoder plugin -->
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.min.js"></script>
<link
rel="stylesheet"
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.css"
type="text/css"
/>
Turf.js: Library for geospatial calculations. You will need this library if you want to calculate distance.
<!-- Turf.js plugin -->
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
Here are the scripts combined that you can copy and paste into the head section of your Webflow project.
<!-- Mapbox Resources -->
<script src='https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.css' rel='stylesheet' />
<!-- Geocoder plugin -->
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.min.js"></script>
<link
rel="stylesheet"
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.css"
type="text/css"
/>
<!-- Turf.js plugin -->
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
Next, we will move on to the HTML and CSS you need to add to your project.
Overall Structure
Here is the overall structure of the project. I’ll break down each major section below. But first take note of the overall structure. It is also important to note that I use Finsweet’s Client First as a naming convention.
If you are familiar with that system, you will know what styles are applied to each div as you work down to the map component.
If you are unfamiliar, please read more about Client First.
Section
Give the section the map is in an ID of section-map
Container
I gave my container a combo class of .is-map
and styled it differently from my other containers on the site. I initially did this since the map was 100% width, but decided to shrink it to fit the normal container on the website.
I gave the section container a min-height: 36.875rem
of and I used clamp for the width. Here is the clamp setting I found to work the best: clamp(50vw, 100%, 80rem)
I am sure there are other ways to control the responsive nature and size of the map, but this is what I found to be the best for my setup.
Map Component
The map component is that which wraps the map and the sidebar.
The map component is set to flex-direction: row
, no gap
, width: 100%
and a position: relative
Mapbox Wrapper
Add a element in your HTML where the map will be rendered. This element should have an id of map.
To give the .mapbox-wrapper
dimensions, I used an Aspect Ratio of widescreen and a custom Flex Child sizing property of Shrink 1
with a 100% basis
. I chose these dimensions because I created a scrollable sidebar.
<div id="map"></div>
Sidebar
I chose to add a scrollable sidebar. The sidebar is driven by Webflow’s CMS and gives visitors the opportunity to scroll through a list of locations.
CSS
I gave the side bar a custom Flex Child sizing property of shrink 1
with a 25% basis
It is also set to a max-height: 39.375 rem
and overflow: scroll
These setting are what I used on Desktop and iPad. On mobile landscape and mobile portrait, I moved the sidebar below the map. As a result, I have styled it differently so that it takes up the entire container.
JavaScript Scroll to Top of Section
It is also important to not that I added some code that scrolls back to the top of the section when a sidebar item is clicked on mobile views. This way you can see the popup or extra information on the map.
The scroll only occurs on mobile landscape and mobile portrait since the map is out of view for many of the locations.
In order for the scroll functionality to work, you need to give the section the map is in an ID of section-map
For reference, here is the portion of the Script where the scroll to section occurs. It is at the end of the larger script.
// Add event listeners to the sidebar location items
locationItemSidebar.forEach((location) => {
// Add a click event listener to each sidebar location item
location.addEventListener("click", (e) => {
// Get the location ID from the data-id attribute of the clicked sidebar item
const locationID = e.currentTarget.getAttribute("data-id");
// Log the location ID for debugging purposes
console.log(
`This is the locationID: ${locationID} that corresponds to the clicked feature.`
);
//SCROLL TO TOP OF SECTION
// Check if the screen width is 767px or below (mobile view)
if (window.innerWidth <= 767) {
// Scroll to the section with ID 'section-map' to ensure the map is visible on mobile view
document.getElementById("section-map").scrollIntoView({
behavior: "smooth",
});
}
// Find the feature in the GeoJSON data that matches the location ID
const feature = stores.features.find(
(feature) => feature.properties.id === locationID
);
// If the feature is found in the GeoJSON data
if (feature) {
// Get the coordinates and description of the feature
const coordinates = feature.geometry.coordinates;
const description = feature.properties.description;
// Create a mock event object to pass to the addPopup function
const mockEvent = {
features: [
{
geometry: { coordinates: coordinates },
properties: { description: description },
},
],
lngLat: { lng: coordinates[0], lat: coordinates[1] },
};
// Add a popup at the feature's location
addPopup(mockEvent);
// Update the active location in the sidebar
updateActiveLocation(locationID);
// Show the corresponding map card
showMapCard(locationID);
// Zoom to the feature's location on the map
zoomToLocation(map, coordinates);
} else {
// Log an error message if the feature is not found in the GeoJSON data
console.error(`Feature with ID ${locationID} not found.`);
}
});
});
Here is the view I created for the mobile landscape and mobile portrait.
GeoJSON Data
The sidebar collection list is the collection list from which the GeoJSON data is being built.
If you don’t want or don’t need a sidebar, you will still need to include a collection list on the page with the following attributes in order for the code to work properly.
This collection list can be hidden, but it will need to be on the page in order for the GeoJSON data to be built.
NOTE: The HTML below is not the complete HTML (see image for complete HTML structure). Instead, it represents the elements that need attributes added to them for functionality purposes.
The attribute is listed before the class. These attributes are added in the settings panel on the right hand side. No value is needed, just the name.
Your custom attribute will look like the example after you have added it.
<!-- Sidebar Elements That Have Attributes Added -->
<div location-list-sidebar class="location-list_sidebar" >
<div location-collection-item-sidebar class="location-item_sidebar" >
<div location-item-sidebar class="location-item-wrapper_sidebar" >
<div location-image class="location-image-wrapper_sidebar">
<div location-name-sidebar class="location-name_sidebar">Location Name</div>
<div location-distance-sidebar class="location-distance_sidebar">
<div location-latitude-sidebar class="location-latitude_sidebar">Latitude</div>
<div location-longitude-sidebar class="location-longitude_sidebar">Longitude</div>
Latitude and Longitude
Be sure to include the latitude and longitude elements in your collection list.
You can hide them. The html element won’t be visible on the screen, but will still be in the source code, allowing the script to pull the latitude and longitude for each location and combine them into coordinates.
Adding an Active Class
In order to tell visitors which item is currently selected, I have added a combo class to .location-item-wrapper_sidebar
which is .is--active
This class applies a grey background to the item in the sidebar that is active. Style it how you see fit for your project.
Distance Div
You will want to include a div in your sidebar card where the distance will be displayed once calculated.
The project is set to calculate distance from the user when they enter their address into the search bar or use the locator button.
When the users take that action, the list will reorder based on distance. Each store’s distance from the user’s current location will also be displayed on the card.
You will need to make sure to include an empty div in your card so that the distance can be inserted when the user takes those actions.
As shown above, this div gets an attribute of <div location-distance-sidebar class="location-distance_sidebar">
Map Card
I have chosen to add a separate map card that popups when a location is selected. This is not a necessary step and you don’t need to include it in your project.
I added the additional card to show more information about each store, particularly the hours of operation along with tasting hours with a tabbed menu.
If your project doesn’t require a lot of information to be shown about each store, you can use the popup on the map to display the necessary information. Doing so would provide a more simplified approach to the map setup. I’ll point out where you can add this information later in the post.
If you choose not to include this in your project, you’ll need to remove it’s presence from the JavaScript code so you don’t get any errors.
Combo Class
When setting up the HTML and CSS be sure to set both the .location-map-card_wrapper
and the .location-map-card_item
to display none when you finish styling them.
Also, add a combo class of .is--show
to both elements.
The combo class should apply the display property you used before hiding the collection list and the card. We’ll apply the combo class .is--show
later in our Javascript to show the card.
Attributes
Just like with our sidebar collection list, we’ll need to apply attributes to the card collection list.
<!-- Map Card Elements That Have Attributes added -->
<div locations-map-card-wrapper class="locations-map-card_wrapper">
<div locations-map-card-item class="locations-map-card_item">
<div locations-map-card-close-block class="locations-map-card_close-block">
Style
I’m not going to go through how to style a card as that will changed based on each project.
You can see the style I have chosen below.
I have also added a tab component to the card to show both the store hours and the tasting hours for each store.
But style your card as you see fit. Just be sure to include the necessary attributes to the card for functionality purposes.
MapBox CSS - Popup
Include the following CSS in the head of your page. It controls the style for the popup on the map.
These are the base styles. If you decide to show information in this popup other than the location name, which is what I have done, your CSS for the popup would be more extensive.
<style>
/*style map popups*/
.mapboxgl-popup-content {
pointer-events: auto;
border-radius: 4px;
box-shadow: none;
padding: 12px 16px;
color: var(--text-color--text-primary);
background-color: #fcfcfc;
font-weight: 500;
}
/*popup bottom arrow color*/
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
border-top-color: #fcfcfc;
}
</style>
Next, let's move on to the Code Breakdown section. Here I’ll provide a detailed explanation of the script's functionality, starting with the initialization part.
Code Breakdown
Here is the full JavaScript code to paste into your project. A full breakdown of this code will follow. But if you want to setup your project like I have mine, copy and paste this code into the body section of your project.
<script>
//This script correlates everything based on the city name in the CMS. The location ID is based on this city name, which means the city name needs to be unique. It cannot be a duplicate. If you find it is being duplicated, can use another selector that will not be duplicated in the future.
window.Webflow ||= [];
window.Webflow.push(() => {
//////////////////////////////////////////////////////////////
/////////////////////// VARIABLES ////////////////////////////
// Variables for the map card wrapper, items, and close buttons
const locationMapCardWrapper = document.querySelector(
"[locations-map-card-wrapper]"
);
const locationMapCardItem = document.querySelectorAll(
"[locations-map-card-item]"
);
const locationMapCardCloseBtn = document.querySelectorAll(
"[locations-map-card-close-block]"
);
// Variables for the sidebar items and popups
const locationItemSidebar = document.querySelectorAll(
"[location-item-sidebar]"
);
const popUps = document.getElementsByClassName("mapboxgl-popup");
// Remove the 'is--show' class from the map card wrapper
locationMapCardWrapper.classList.remove("is--show");
// Set the Mapbox access token for authentication
mapboxgl.accessToken =
"YOUR ACCESS TOKEN";
//////////////////////////////////////////////////////////////
/////////////////// INITIALIZE MAPBOX MAP ////////////////////
// Initialize the Mapbox map within the element with id 'map'
const map = new mapboxgl.Map({
container: "map", // The id of the HTML element to initialize the map in
style: "YOUR MAPBOX STYLE", // The Mapbox style to use
center: [-97.022553, 32.771663], // Initial center coordinates [longitude, latitude]
zoom: 10.25, // Initial zoom level
});
// Adjust the zoom level of the map based on the screen size
let mq = window.matchMedia("(max-width: 767px)");
if (mq.matches) {
map.setZoom(8); // Set map zoom level for mobile size
}
//////////////////////////////////////////////////////////////
/////////// SORT LIST BASED ON CITY NAME USING ATTRIBUTE /////
/* NOTE: THIS IS A FALL BACK. YOU CAN COMMENT OUT IF YOU DO THE FOLLOWING: IF you use Webflow to sort, you have to sort both the sidebar list and the card list. If you sort one by city name A-Z and not the other, the correct data-attribute that correlates the two lists will not be added and there will be a mismatch.*/
// Sort the NodeList of location items based on the 'location-name-sidebar' attribute
const sortedLocations = Array.from(locationItemSidebar).sort((a, b) => {
const nameA = a
.querySelector("[location-name-sidebar]")
.textContent.trim()
.toUpperCase();
const nameB = b
.querySelector("[location-name-sidebar]")
.textContent.trim()
.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
});
// Select the parent element with the attribute location-list-sidebar and remove all existing children
const parentElement = document.querySelector("[location-list-sidebar]");
while (parentElement.firstChild) {
parentElement.removeChild(parentElement.firstChild);
}
// Reorder the HTML elements based on the sorted order
sortedLocations.forEach((location) => {
parentElement.appendChild(location); // This moves the element to the end of the parent, effectively reordering them
});
//////////////////////////////////////////////////////////////
/////////////////// CREATE GEOJSON DATA //////////////////////
// Create an empty GeoJSON object to store location data
let stores = {
type: "FeatureCollection",
features: [],
};
// Get the list of location elements from the HTML and convert each to GeoJSON
const listLocations = locationItemSidebar;
// Function to convert each location element into GeoJSON and add to stores object
const getGeoData = function () {
// Loop through each location in the list
listLocations.forEach(function (location) {
// Get the latitude from the element and trim any whitespace
const locationLat = location
.querySelector("[location-latitude-sidebar]")
.textContent.trim();
// Get the longitude from the element and trim any whitespace
const locationLong = location
.querySelector("[location-longitude-sidebar]")
.textContent.trim();
// Create coordinates array from longitude and latitude using parseFloat to convert strings to numbers
const coordinates = [parseFloat(locationLong), parseFloat(locationLat)];
// Get the location ID from the element (using the location name)
const locationID = location.querySelector(
"[location-name-sidebar]"
).textContent;
// Get the location info for popup content on the map (using the location name)
const locationCity = location.querySelector(
"[location-name-sidebar]"
).textContent;
// Create a GeoJSON feature for the location using the gathered information
const geoData = {
type: "Feature",
geometry: {
type: "Point",
coordinates: coordinates,
},
properties: {
id: locationID,
city: locationCity, // information used in the popup on the map
},
};
// Add the feature to the stores object if it's not already included
if (!stores.features.includes(geoData)) {
stores.features.push(geoData);
}
// Set the data-id attribute on the location element for later reference in sidebar click events
location.setAttribute("data-id", locationID);
});
// Log the stores object to the console for debugging
console.log(stores);
};
// Call getGeoData function to turn Webflow CMS items into GeoJSON Data for Mapbox to use
getGeoData();
// Set data-id attribute for each map card item based on the corresponding location ID
locationMapCardItem.forEach((el, i) => {
// Get the location ID from the corresponding location in the list
const locationID = listLocations[i]
.querySelector("[location-name-sidebar]") // Find the element with location name
.textContent.trim(); // Get the text content and trim any whitespace
// Set the data-id attribute on the map card item using the location ID
el.setAttribute("data-id", locationID);
});
//////////////////////////////////////////////////////////////
///////////////// RENDER LOCATIONS LAYER ON THE MAP //////////
// Function to add the GeoJSON data as a layer to the map
const addMapPoints = function () {
map.addLayer({
id: "locations", // Layer id
type: "circle", // Layer type (circle for point features)
source: {
type: "geojson", // Source type
data: stores, // Uses GeoJSON data from the stores object
},
paint: {
"circle-radius": 8, // Circle radius
"circle-stroke-width": 1, // Circle stroke width
"circle-color": "#EB2D2E", // Circle fill color
"circle-opacity": 1, // Circle opacity
"circle-stroke-color": "#CB1F10", // Circle stroke color
},
});
};
/////////////////////////////////////////////////////////////////////////////
/////// Helper function to calculate distances and update the DOM //////////
const calculateDistancesAndUpdateDOM = (referencePoint) => {
const options = { units: "miles" }; // Set the units for distance calculation to miles
// Loop through each location in the list
listLocations.forEach((location) => {
// Get the latitude from the element and trim any whitespace
const locationLat = location
.querySelector("[location-latitude-sidebar]")
.textContent.trim();
// Get the longitude from the element and trim any whitespace
const locationLong = location
.querySelector("[location-longitude-sidebar]")
.textContent.trim();
// Create coordinates array from longitude and latitude using parseFloat to convert strings to numbers
const coordinates = [parseFloat(locationLong), parseFloat(locationLat)];
// Create a GeoJSON feature for the location using the gathered coordinates
const locationGeoJSON = {
type: "Point",
coordinates: coordinates,
};
// Calculate the distance between the reference point and the location
const distance = turf.distance(referencePoint, locationGeoJSON, options);
// Store the calculated distance as a data attribute on the location element
location.setAttribute("data-distance", distance);
// Find or create a distance element to display the distance
let distanceElement = location.querySelector(
"[location-distance-sidebar]"
);
if (!distanceElement) {
distanceElement = document.createElement("div"); // Create a new div element if it doesn't exist
distanceElement.className = "location-distance_sidebar"; // Set the class name of the div
location.appendChild(distanceElement); // Append the div to the location element
}
// Set the text content of the div to the calculated distance in miles
distanceElement.textContent = `${distance.toFixed(2)} miles`;
});
// Sort the locations based on the distance attribute
const sortedLocations = Array.from(listLocations).sort((a, b) => {
return (
parseFloat(a.getAttribute("data-distance")) -
parseFloat(b.getAttribute("data-distance"))
);
});
// Select the parent element with the attribute location-list-sidebar and remove all existing children
const parentElement = document.querySelector("[location-list-sidebar]");
while (parentElement.firstChild) {
parentElement.removeChild(parentElement.firstChild);
}
// Reorder the HTML elements based on the sorted order
sortedLocations.forEach((location) => {
parentElement.appendChild(location); // Append the location to the parent element
});
// Return the sorted locations
return sortedLocations;
};
/////////////////////////////////////////////////////////////////////////////////////
/////// Helper function to highlight the closest location and add a popup //////////
const highlightClosestLocationAndAddPopup = (sortedLocations) => {
// Get the closest location from the sorted list (first element)
const closestLocation = sortedLocations[0];
// Check if there is a closest location
if (closestLocation) {
// Add the 'is--active' class to highlight the closest location
closestLocation.classList.add("is--active");
// Get the ID of the closest location from the data-id attribute
const ID = closestLocation.getAttribute("data-id");
// Find the feature in the GeoJSON data that matches the closest location ID
const feature = stores.features.find(
(feature) => feature.properties.id === ID
);
// Check if the feature is found
if (feature) {
// Extract the coordinates and city of the feature
const coordinates = feature.geometry.coordinates;
const city = feature.properties.city;
// Create a mock event object to pass to the addPopup function
const mockEvent = {
features: [
{
geometry: { coordinates: coordinates },
properties: { city: city },
},
],
lngLat: { lng: coordinates[0], lat: coordinates[1] }, // Set the lngLat property of the mock event
};
// Add the popup to the map at the closest location
addPopup(mockEvent);
// Update the active location in the sidebar to highlight it
updateActiveLocation(ID);
// Show the detailed map card for the closest location
showMapCard(ID);
// Zoom in to the closest location on the map
zoomToLocation(map, coordinates);
} else {
// Log an error message if the feature is not found in the GeoJSON data
console.error(`Feature with ID ${ID} not found.`);
}
}
};
// Event listener for when the map is loaded
map.on("load", function () {
// Add map points after map loads
addMapPoints();
//////////////////////////////////////////////////////////////
///////////////// MAP GEOCODER FUNCTIONALITY (SEARCH) //////////
// Initialize the Mapbox Geocoder for search functionality
const geocoder = new MapboxGeocoder({
accessToken: mapboxgl.accessToken, // Set the access token for Mapbox
mapboxgl: mapboxgl, // Reference to the Mapbox GL JS library
placeholder: "Type your address" // Set the placeholder text for the search box
});
// Add the geocoder control to the map
map.addControl(geocoder);
// Add zoom and rotation controls to the map
map.addControl(new mapboxgl.NavigationControl());
// Event listener that fires when a search result occurs
geocoder.on("result", (event) => {
// Extract the geometry of the search result (coordinates)
const searchResult = event.result.geometry;
// Calculate distances from the search result to each location and update the DOM
// This function returns the sorted list of locations based on their distance to the search result
const sortedLocations = calculateDistancesAndUpdateDOM(searchResult);
// Highlight the closest location from the sorted list and add a popup
highlightClosestLocationAndAddPopup(sortedLocations);
});
//////////////////////////////////////////////////////////////
//////// MAP GEOLOCATE FUNCTIONALITY (CURRENT POSITION) /////
// Initialize the GeolocateControl, which provides a button that when clicked
// uses the browser's geolocation API to locate the user on the map
// Initialize the Mapbox GeolocateControl for tracking user's location
const geolocate = new mapboxgl.GeolocateControl({
// Configuration options for geolocation
positionOptions: {
enableHighAccuracy: true, // Enable high accuracy for geolocation
},
trackUserLocation: false, // Do not continuously track user's location
showUserHeading: true, // Show the direction the user is facing
});
// Add the geolocate control to the map
map.addControl(geolocate);
// Event listener that fires when a geolocation event occurs (i.e., when the user's location is found)
geolocate.on("geolocate", (event) => {
// Log to the console that a geolocation event has occurred
console.log("A geolocate event has occurred.");
// Extract the user's current coordinates (longitude and latitude) from the event object
const geolocateResult = [event.coords.longitude, event.coords.latitude];
// Calculate distances from the user's current location to each listed location and update the DOM
// This function returns the sorted list of locations based on their distance to the user's current location
const sortedLocations = calculateDistancesAndUpdateDOM({
type: "Point", // Specify the GeoJSON type as Point
coordinates: geolocateResult, // Set the coordinates to the user's current location
});
// Highlight the closest location from the sorted list and add a popup
highlightClosestLocationAndAddPopup(sortedLocations);
});
});
//////////////////////////////////////////////////////////////
///////////// OPTIONS FOR THE MAP ////////////////////////////
// Popup options
const popupOptions = {
closeOnClick: false,
};
//////////////////////////////////////////////////////////////
///////////// FUNCTIONS BASED ON CLICK EVENTS ////////////////
// Function to close the detailed map card when the close button is clicked
const locationMapCardClose = function () {
// Remove the 'is--show' class from the map card wrapper to hide it
locationMapCardWrapper.classList.remove("is--show");
// Iterate over each map card item
locationMapCardItem.forEach((el) => {
// Remove the 'is--show' class from each map card item to ensure none of them are visible
el.classList.remove("is--show");
});
};
// Function to add a popup to the dot on the map. Event properties are passed from click event
const addPopup = function (e) {
// Extract the coordinates of the clicked feature and create a copy of the coordinates array
const coordinates = e.features[0].geometry.coordinates.slice();
// Extract the city of the clicked feature for the popup content
const city = e.features[0].properties.city;
// Adjust coordinates if the map is zoomed out and the popup appears on a different copy of the feature
// This ensures the popup appears on the correct side of the map
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
// Check if there is already a popup on the map and if so, remove it to avoid multiple popups
if (popUps[0]) popUps[0].remove();
// Create and display the popup at the coordinates with the city
const popup = new mapboxgl.Popup(popupOptions)
.setLngLat(coordinates) // Set the longitude and latitude for the popup
.setHTML(city) // Set the HTML content of the popup
.addTo(map); // Add the popup to the map
// Add event listener to close card items when popup is closed
popup.on("close", () => {
locationMapCardClose(); // Close the detailed map card
console.log("popup was closed"); // Log that the popup was closed
});
};
// Function to update the active item in the sidebar by adding the 'is--active' class
const updateActiveLocation = function (locationID) {
// Remove the 'is--active' class from all sidebar location items
locationItemSidebar.forEach((el) => el.classList.remove("is--active"));
// Loop through each sidebar location item to find the one with the matching locationID
locationItemSidebar.forEach((el) => {
// Check if the current item's data-id attribute matches the provided locationID
if (el.getAttribute("data-id") === locationID) {
// Add the 'is--active' class to the matching item
el.classList.add("is--active");
}
});
};
// Function to show the detailed map card for a specific location
const showMapCard = function (locationID) {
console.log(`Showing map card for location ID: ${locationID}`);
// Add the 'is--show' class to the map card wrapper to display it
locationMapCardWrapper.classList.add("is--show");
// Loop through each map card item and remove the 'is--show' class
locationMapCardItem.forEach((el) => {
el.classList.remove("is--show");
/*console.log(
`Removed 'is--show' class from card with data-id: ${el.getAttribute(
"data-id"
)}`
);*/
});
// Loop through each map card item to find the one with the matching locationID
locationMapCardItem.forEach((el) => {
/* console.log(
`Checking card with data-id: ${el.getAttribute(
"data-id"
)} against locationID: ${locationID}`
);*/
// Check if the current item's data-id attribute matches the provided locationID
if (el.getAttribute("data-id") === locationID) {
// Add the 'is--show' class to the matching item to display it
el.classList.add("is--show");
/*console.log(
`Added 'is--show' class to card with data-id: ${el.getAttribute(
"data-id"
)}`
);*/
}
});
};
// Fly to location on the map and zoom in - Adjust properties for different effects
const zoomToLocation = function (map, coordinates) {
map.flyTo({
center: coordinates, // Center the map on the provided coordinates
zoom: 14, // Set the zoom level to 14 for a closer view
speed: 1, // Set the animation speed (1 is default, higher is faster)
curve: 1, // Set the animation curve (1 is default, higher is more curved)
easing(t) {
return t; // Set the easing function for the animation (t is the current time)
},
});
};
//////////////////////////////////////////////////////////////
//////////////////// EVENT LISTENERS /////////////////////////
// Listens for clicks on the location layer of the map (dots on the map)
map.on("click", "locations", (e) => {
// Get the location ID from the clicked feature's properties
const locationID = e.features[0].properties.id;
// Log the location ID for debugging purposes
console.log(
`This is the location ID: ${locationID} that corresponds to the clicked feature.`
);
// Add a popup to the map at the location of the clicked feature
addPopup(e);
// Update the active location in the sidebar to highlight the clicked location
updateActiveLocation(locationID);
// Show the detailed map card for the clicked location
showMapCard(locationID);
// Zoom in to the clicked location on the map
zoomToLocation(map, e.features[0].geometry.coordinates);
});
// Changes cursor style when cursor moves onto the map layer "locations" (REMEMBER: Locations has the dots so when you hover over a dot, the cursor changes)
map.on("mouseenter", "locations", () => {
map.getCanvas().style.cursor = "pointer";
});
// Reverses cursor style when cursor moves off the map layer "locations" (REMEMBER: Locations has the dots so when you hover off a dot, the cursor changes)
map.on("mouseleave", "locations", () => {
map.getCanvas().style.cursor = "";
});
// Add Event Listener that closes the detailed card when the close button is clicked on the detailed card
locationMapCardCloseBtn.forEach((btn) => {
btn.addEventListener("click", () => {
locationMapCardClose();
});
});
// Add event listeners to the sidebar location items
locationItemSidebar.forEach((location) => {
// Add a click event listener to each sidebar location item
location.addEventListener("click", (e) => {
// Get the location ID from the data-id attribute of the clicked sidebar item
const locationID = e.currentTarget.getAttribute("data-id");
// Log the location ID for debugging purposes
console.log(
`This is the locationID: ${locationID} that corresponds to the clicked feature.`
);
// Check if the screen width is 767px or below (mobile view)
if (window.innerWidth <= 767) {
// Scroll to the section with ID 'section-map' to ensure the map is visible on mobile view
document.getElementById("section-map").scrollIntoView({
behavior: "smooth",
});
}
// Find the feature in the GeoJSON data that matches the location ID
const feature = stores.features.find(
(feature) => feature.properties.id === locationID
);
// If the feature is found in the GeoJSON data
if (feature) {
// Get the coordinates and city of the feature
const coordinates = feature.geometry.coordinates;
const city = feature.properties.city;
// Create a mock event object to pass to the addPopup function
const mockEvent = {
features: [
{
geometry: { coordinates: coordinates },
properties: { city: city },
},
],
lngLat: { lng: coordinates[0], lat: coordinates[1] },
};
// Add a popup at the feature's location
//addPopup(mockEvent); This function is currently causing a bug - The screen is jumping around. It has to do with the close button on the popup being focused automatically. Will seek a solution and update, but for now, leave this commented out.
// Update the active location in the sidebar
updateActiveLocation(locationID);
// Show the corresponding map card
showMapCard(locationID);
// Zoom to the feature's location on the map
zoomToLocation(map, coordinates);
} else {
// Log an error message if the feature is not found in the GeoJSON data
console.error(`Feature with ID ${locationID} not found.`);
}
});
});
});
</script>
Initialization
// This script correlates everything based on the city name in the CMS. The location ID is based on this city name, which means the city name needs to be unique. It cannot be a duplicate. If you find it is being duplicated, you can use another selector that will not be duplicated in the future.
window.Webflow ||= [];
window.Webflow.push(() => {
//////////////////////////////////////////////////////////////
/////////////////////// VARIABLES ////////////////////////////
// Variables for the map card wrapper, items, and close buttons
const locationMapCardWrapper = document.querySelector("[locations-map-card-wrapper]");
const locationMapCardItem = document.querySelectorAll("[locations-map-card-item]");
const locationMapCardCloseBtn = document.querySelectorAll("[locations-map-card-close-block]");
// Variables for the sidebar items and popups
const locationItemSidebar = document.querySelectorAll("[location-item-sidebar]");
const popUps = document.getElementsByClassName("mapboxgl-popup");
// Remove the 'is--show' class from the map card wrapper
locationMapCardWrapper.classList.remove("is--show");
// Set the Mapbox access token for authentication
mapboxgl.accessToken = "YOUR ACCESS TOKEN";
//////////////////////////////////////////////////////////////
/////////////////// INITIALIZE MAPBOX MAP ////////////////////
// Initialize the Mapbox map within the element with id 'map'
const map = new mapboxgl.Map({
container: "map", // The id of the HTML element to initialize the map in
style: "YOUR MAPBOX MAP STYLE", // The Mapbox style to use
center: [-97.022553, 32.771663], // Initial center coordinates [longitude, latitude]
zoom: 10.25, // Initial zoom level
});
// Adjust the zoom level of the map based on the screen size
let mq = window.matchMedia("(max-width: 767px)");
if (mq.matches) {
map.setZoom(8); // Set map zoom level for mobile size
}
Explanation:
- This script starts by ensuring the code runs within the Webflow environment.
- It initializes variables for the map card elements, sidebar items, and popups.
- The Mapbox access token is set to authenticate the map.
- The Mapbox map is initialized with a specific style, center coordinates, and zoom level. The zoom level is adjusted for mobile devices.
Let's continue with the next part of the code, covering sorting locations and creating GeoJSON data.
Sorting Locations
THIS IS A FALL BACK. YOU CAN COMMENT OUT or you don’t need it IF YOU DO THE FOLLOWING:
- IF you use Webflow to sort, you have to sort both the sidebar list and the card list.
- If you sort one by city name A-Z and not the other, the correct data-attribute that correlates the two lists will not be added and there will be a mismatch.
//////////////////////////////////////////////////////////////
/////////// SORT LIST BASED ON CITY NAME USING ATTRIBUTE /////
/* NOTE: THIS IS A FALL BACK. YOU CAN COMMENT OUT IF YOU DO THE FOLLOWING: IF you use Webflow to sort, you have to sort both the sidebar list and the card list. If you sort one by city name A-Z and not the other, the correct data-attribute that correlates the two lists will not be added and there will be a mismatch.*/
// Sort the NodeList of location items based on the 'location-name-sidebar' attribute
const sortedLocations = Array.from(locationItemSidebar).sort((a, b) => {
const nameA = a
.querySelector("[location-name-sidebar]")
.textContent.trim()
.toUpperCase();
const nameB = b
.querySelector("[location-name-sidebar]")
.textContent.trim()
.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
});
// Select the parent element with the attribute location-list-sidebar and remove all existing children
const parentElement = document.querySelector("[location-list-sidebar]");
while (parentElement.firstChild) {
parentElement.removeChild(parentElement.firstChild);
}
// Reorder the HTML elements based on the sorted order
sortedLocations.forEach((location) => {
parentElement.appendChild(location); // This moves the element to the end of the parent, effectively reordering them
});
Explanation:
Sorting Locations:
- The script sorts the location items alphabetically based on the 'location-name-sidebar' attribute.
- It converts the NodeList to an array, sorts the array, and then reorders the HTML elements accordingly.
Create GeoJSON Data
This data is created dynamically from the Webflow CMS. Ensure you have a collection list on the page with the necessary elements (See above setup section for more details).
//////////////////////////////////////////////////////////////
/////////////////// CREATE GEOJSON DATA //////////////////////
// Create an empty GeoJSON object to store location data
let stores = {
type: "FeatureCollection",
features: [],
};
// Get the list of location elements from the HTML and convert each to GeoJSON
const listLocations = locationItemSidebar;
// Function to convert each location element into GeoJSON and add to stores object
const getGeoData = function () {
// Loop through each location in the list
listLocations.forEach(function (location) {
// Get the latitude from the element and trim any whitespace
const locationLat = location
.querySelector("[location-latitude-sidebar]")
.textContent.trim();
// Get the longitude from the element and trim any whitespace
const locationLong = location
.querySelector("[location-longitude-sidebar]")
.textContent.trim();
// Create coordinates array from longitude and latitude using parseFloat to convert strings to numbers
const coordinates = [parseFloat(locationLong), parseFloat(locationLat)];
// Get the location ID from the element (using the location name)
const locationID = location.querySelector(
"[location-name-sidebar]"
).textContent;
// Get the location info for popup content on the map (using the location name)
const locationCity = location.querySelector(
"[location-name-sidebar]"
).textContent;
// Create a GeoJSON feature for the location using the gathered information
const geoData = {
type: "Feature",
geometry: {
type: "Point",
coordinates: coordinates,
},
properties: {
id: locationID,
city: locationCity, // information used in the popup on the map
},
};
// Add the feature to the stores object if it's not already included
if (!stores.features.includes(geoData)) {
stores.features.push(geoData);
}
// Set the data-id attribute on the location element for later reference in sidebar click events
location.setAttribute("data-id", locationID);
});
// Log the stores object to the console for debugging
console.log(stores);
};
// Call getGeoData function to turn Webflow CMS items into GeoJSON Data for Mapbox to use
getGeoData();
// Set data-id attribute for each map card item based on the corresponding location ID
locationMapCardItem.forEach((el, i) => {
// Get the location ID from the corresponding location in the list
const locationID = listLocations[i]
.querySelector("[location-name-sidebar]") // Find the element with location name
.textContent.trim(); // Get the text content and trim any whitespace
// Set the data-id attribute on the map card item using the location ID
el.setAttribute("data-id", locationID);
});
Explanation:
- An empty GeoJSON object is created to store the location data.
- The
getGeoData
function loops through each location element, extracting the latitude, longitude, and location ID. - It creates a GeoJSON feature for each location and adds it to the
stores
object. - The
data-id
attribute is set on each location element for reference in click events. - Finally, the function logs the
stores
object for debugging purposes.
Next, let's document the Rendering Locations Layer on the Map and Map Geocoder Functionality (Search) sections.
Rendering Locations Layer on the Map
//////////////////////////////////////////////////////////////
///////////////// RENDER LOCATIONS LAYER ON THE MAP //////////
// Function to add the GeoJSON data as a layer to the map
const addMapPoints = function () {
map.addLayer({
id: "locations", // Layer id
type: "circle", // Layer type (circle for point features)
source: {
type: "geojson", // Source type
data: stores, // Uses GeoJSON data from the stores object
},
paint: {
"circle-radius": 8, // Circle radius
"circle-stroke-width": 1, // Circle stroke width
"circle-color": "#EB2D2E", // Circle fill color
"circle-opacity": 1, // Circle opacity
"circle-stroke-color": "#CB1F10", // Circle stroke color
},
});
};
// Event listener for when the map is loaded
map.on("load", function () {
// Add map points after map loads
addMapPoints();
Explanation:
- The
addMapPoints
function adds a new layer to the Mapbox map using the GeoJSON data stored in thestores
object. - The layer is configured to display the points as circles with specified styling properties.
- Update the style to change how yours look.
- You can also add the default Map Marker or your own Map Marker. You can follow MapBox’s add custom map markers in Mapbox GL JS tutorial
Comparison between GeoJSON Layer and Custom HTML Markers
- Performance: The GeoJSON layer method is more performant and scalable for large datasets as it uses Mapbox's optimized vector tile rendering. Custom HTML markers can become slow and cumbersome with a large number of points due to DOM manipulation.
- Customization: Custom HTML markers offer greater flexibility in terms of design and interactivity, allowing for rich and complex marker content.
- Use Case:
- GeoJSON Layer: Best for scenarios where you need to display many points efficiently and don't require highly customized marker designs.
- Custom HTML Markers: Best for scenarios where you need unique, interactive, and richly styled markers, and the number of markers is relatively small to moderate.
In summary, the choice between these methods depends on your specific requirements for performance and customization. For large datasets with simple markers, the GeoJSON layer method is preferable. For fewer markers with complex designs, custom HTML markers are more suitable.
👉 We’ve use the GeoJSON layer. Also, please note if you choose to use the Map Marker version with a custom marker, you will need to review the code, as the event listener for the point uses the point layer for functionality purposes.
Map Geocoder Functionality
The GeoCoder functionality uses both the search bar and the location locator button.
Two helper functions are used to assist with this functionality. I’ll highlight them all below as well as provide a detailed breakdown of each.
Let’s start with the helper functions.
Helper function to calculate distances and update the DOM
/////////////////////////////////////////////////////////////////////////////
/////// Helper function to calculate distances and update the DOM //////////
const calculateDistancesAndUpdateDOM = (referencePoint) => {
const options = { units: "miles" }; // Set the units for distance calculation to miles
// Loop through each location in the list
listLocations.forEach((location) => {
// Get the latitude from the element and trim any whitespace
const locationLat = location
.querySelector("[location-latitude-sidebar]")
.textContent.trim();
// Get the longitude from the element and trim any whitespace
const locationLong = location
.querySelector("[location-longitude-sidebar]")
.textContent.trim();
// Create coordinates array from longitude and latitude using parseFloat to convert strings to numbers
const coordinates = [parseFloat(locationLong), parseFloat(locationLat)];
// Create a GeoJSON feature for the location using the gathered coordinates
const locationGeoJSON = {
type: "Point",
coordinates: coordinates,
};
// Calculate the distance between the reference point and the location
const distance = turf.distance(referencePoint, locationGeoJSON, options);
// Store the calculated distance as a data attribute on the location element
location.setAttribute("data-distance", distance);
// Find or create a distance element to display the distance
let distanceElement = location.querySelector(
"[location-distance-sidebar]"
);
if (!distanceElement) {
distanceElement = document.createElement("div"); // Create a new div element if it doesn't exist
distanceElement.className = "location-distance_sidebar"; // Set the class name of the div
location.appendChild(distanceElement); // Append the div to the location element
}
// Set the text content of the div to the calculated distance in miles
distanceElement.textContent = `${distance.toFixed(2)} miles`;
});
// Sort the locations based on the distance attribute
const sortedLocations = Array.from(listLocations).sort((a, b) => {
return (
parseFloat(a.getAttribute("data-distance")) -
parseFloat(b.getAttribute("data-distance"))
);
});
// Select the parent element with the attribute location-list-sidebar and remove all existing children
const parentElement = document.querySelector("[location-list-sidebar]");
while (parentElement.firstChild) {
parentElement.removeChild(parentElement.firstChild);
}
// Reorder the HTML elements based on the sorted order
sortedLocations.forEach((location) => {
parentElement.appendChild(location); // Append the location to the parent element
});
// Return the sorted locations
return sortedLocations;
};
Explanation:
- Setup Distance Calculation:
- Set the units for distance calculation to miles using the
options
object.
- Set the units for distance calculation to miles using the
- Loop Through Locations:
- For each location:
- Extract latitude and longitude values.
- Create a GeoJSON feature for the location.
- Calculate the distance between the reference point and the location.
- Store the calculated distance as a data attribute on the location element.
- Find or create a
distance
element to display the distance and set its content.
- For each location:
- Sort Locations by Distance:
- Convert the list of locations to an array and sort them based on the distance attribute.
- Update the DOM:
- Remove all existing children from the parent element that contains the list of locations.
- Append the sorted locations back to the parent element.
- Return the Sorted Locations:
- Return the sorted list of locations for further processing.
Helper function to highlight the closest location and add a popup
// Helper function to highlight the closest location and add a popup
const highlightClosestLocationAndAddPopup = (sortedLocations) => {
// Get the closest location from the sorted list (first element)
const closestLocation = sortedLocations[0];
// Check if there is a closest location
if (closestLocation) {
// Add the 'is--active' class to highlight the closest location
closestLocation.classList.add("is--active");
// Get the ID of the closest location from the data-id attribute
const ID = closestLocation.getAttribute("data-id");
// Find the feature in the GeoJSON data that matches the closest location ID
const feature = stores.features.find(
(feature) => feature.properties.id === ID
);
// Check if the feature is found
if (feature) {
// Extract the coordinates and city of the feature
const coordinates = feature.geometry.coordinates;
const city = feature.properties.city;
// Create a mock event object to pass to the addPopup function
const mockEvent = {
features: [
{
geometry: { coordinates: coordinates },
properties: { city: city },
},
],
lngLat: { lng: coordinates[0], lat: coordinates[1] }, // Set the lngLat property of the mock event
};
// Add the popup to the map at the closest location
addPopup(mockEvent);
// Update the active location in the sidebar to highlight it
updateActiveLocation(ID);
// Show the detailed map card for the closest location
showMapCard(ID);
// Zoom in to the closest location on the map
zoomToLocation(map, coordinates);
} else {
// Log an error message if the feature is not found in the GeoJSON data
console.error(`Feature with ID ${ID} not found.`);
}
}
};
Explanation:
- Get the Closest Location:
- Retrieve the first element from the sorted list of locations, which is the closest location.
- Highlight the Closest Location:
- If a closest location is found, add the 'is--active' class to highlight it in the sidebar.
- Retrieve Location ID:
- Get the ID of the closest location from its
data-id
attribute.
- Get the ID of the closest location from its
- Find the Feature in GeoJSON:
- Search the GeoJSON data to find the feature that matches the location ID.
- Check if Feature Exists:
- If the feature is found, proceed to extract its coordinates and city information.
- Create Mock Event for Popup:
- Construct a mock event object that contains the feature's geometry and properties to pass to the
addPopup
function.
- Construct a mock event object that contains the feature's geometry and properties to pass to the
- Add Popup and Update UI:
- Call
addPopup
to display a popup at the closest location. - Update the active location in the sidebar using
updateActiveLocation
. - Show the detailed map card for the closest location with
showMapCard
. - Zoom into the closest location on the map with
zoomToLocation
.
- Call
- Error Handling:
- If the feature is not found, log an error message indicating that the feature with the specified ID is missing.
Map GeoCODER Functionality (Search)
//////////////////////////////////////////////////////////////
/////////////// MAP GEOCODER FUNCTIONALITY (SEARCH) //////////
// Initialize the Mapbox Geocoder for search functionality
const geocoder = new MapboxGeocoder({
accessToken: mapboxgl.accessToken, // Set the access token for Mapbox
mapboxgl: mapboxgl, // Reference to the Mapbox GL JS library
placeholder: "Type your address" // Set the placeholder text for the search box
});
// Add the geocoder control to the map
map.addControl(geocoder);
// Add zoom and rotation controls to the map
map.addControl(new mapboxgl.NavigationControl());
// Event listener that fires when a search result occurs
geocoder.on("result", (event) => {
// Extract the geometry of the search result (coordinates)
const searchResult = event.result.geometry;
// Calculate distances from the search result to each location and update the DOM
// This function returns the sorted list of locations based on their distance to the search result
const sortedLocations = calculateDistancesAndUpdateDOM(searchResult);
// Highlight the closest location from the sorted list and add a popup
highlightClosestLocationAndAddPopup(sortedLocations);
});
Explanation:
- Initialize Geocoder:
const geocoder = new MapboxGeocoder(...)
: Initializes the Mapbox Geocoder which provides search functionality for the map.accessToken: mapboxgl.accessToken
: Sets the access token for authenticating with the Mapbox API.mapboxgl: mapboxgl
: References the Mapbox GL JS library.
- Add Geocoder Control:
map.addControl(geocoder)
: Adds the geocoder control to the map, allowing users to search for locations.
- Add Navigation Control:
map.addControl(new mapboxgl.NavigationControl())
: Adds navigation controls (zoom and rotation) to the map for better user interaction.
- Event Listener for Geocoder Result:
geocoder.on("result", (event) => {...})
: Adds an event listener that triggers when a search result is selected from the geocoder.
- Extract Search Result Geometry:
const searchResult = event.result.geometry
: Extracts the geometry (coordinates) of the search result from the event object.
- Calculate Distances and Update DOM:
const sortedLocations = calculateDistancesAndUpdateDOM(searchResult)
: Calls a helper function that calculates the distances from the search result to each location and updates the DOM accordingly. This function returns the sorted list of locations based on their distances.
- Highlight Closest Location and Add Popup:
highlightClosestLocationAndAddPopup(sortedLocations)
: Calls another helper function that highlights the closest location in the sidebar and adds a popup to the map at the location of the closest point.
Map Geolocate Functionality (Current Position)
//////////////////////////////////////////////////////////////
//////// MAP GEOLOCATE FUNCTIONALITY (CURRENT POSITION) /////
// Initialize the Mapbox GeolocateControl for tracking user's location
const geolocate = new mapboxgl.GeolocateControl({
// Configuration options for geolocation
positionOptions: {
enableHighAccuracy: true, // Enable high accuracy for geolocation
},
trackUserLocation: false, // Do not continuously track user's location
showUserHeading: true, // Show the direction the user is facing
});
// Add the geolocate control to the map
map.addControl(geolocate);
// Event listener that fires when a geolocation event occurs (i.e., when the user's location is found)
geolocate.on("geolocate", (event) => {
// Log to the console that a geolocation event has occurred
console.log("A geolocate event has occurred.");
// Extract the user's current coordinates (longitude and latitude) from the event object
const geolocateResult = [event.coords.longitude, event.coords.latitude];
// Calculate distances from the user's current location to each listed location and update the DOM
// This function returns the sorted list of locations based on their distance to the user's current location
const sortedLocations = calculateDistancesAndUpdateDOM({
type: "Point", // Specify the GeoJSON type as Point
coordinates: geolocateResult, // Set the coordinates to the user's current location
});
// Highlight the closest location from the sorted list and add a popup
highlightClosestLocationAndAddPopup(sortedLocations);
});
Explanation:
- Initialize GeolocateControl:
const geolocate = new mapboxgl.GeolocateControl(...)
: Initializes the Mapbox GeolocateControl which provides geolocation functionality for the map.positionOptions: { enableHighAccuracy: true }
: Enables high accuracy for the geolocation.trackUserLocation: false
: Disables continuous tracking of the user's location.showUserHeading: true
: Displays the direction the user is facing.
- Add Geolocate Control:
map.addControl(geolocate)
: Adds the geolocate control to the map, allowing users to find their location.
- Event Listener for Geolocate Event:
geolocate.on("geolocate", (event) => {...})
: Adds an event listener that triggers when the user's location is found.
- Log Geolocation Event:
console.log("A geolocate event has occurred.")
: Logs to the console that a geolocation event has occurred for debugging purposes.
- Extract User's Coordinates:
const geolocateResult = [event.coords.longitude, event.coords.latitude]
: Extracts the user's current coordinates (longitude and latitude) from the event object.
- Calculate Distances and Update DOM:
const sortedLocations = calculateDistancesAndUpdateDOM(...)
: Calls a helper function that calculates the distances from the user's current location to each listed location and updates the DOM accordingly. This function returns the sorted list of locations based on their distances.
- Highlight Closest Location and Add Popup:
highlightClosestLocationAndAddPopup(sortedLocations)
: Calls another helper function that highlights the closest location in the sidebar and adds a popup to the map at the location of the closest point.
Functions Based on Click Events
Close Map Card
//////////////////////////////////////////////////////////////
///////////// FUNCTIONS BASED ON CLICK EVENTS ////////////////
// Function to close the detailed map card when the close button is clicked
const locationMapCardClose = function () {
// Remove the 'is--show' class from the map card wrapper to hide it
locationMapCardWrapper.classList.remove("is--show");
// Iterate over each map card item
locationMapCardItem.forEach((el) => {
// Remove the 'is--show' class from each map card item to ensure none of them are visible
el.classList.remove("is--show");
});
};
Explanation:
- The
locationMapCardClose
function removes theis--show
class from the map card wrapper and each map card item, effectively hiding them.
Add Popup
// Function to add a popup to the dot on the map. Event properties are passed from click event
const addPopup = function (e) {
// Extract the coordinates of the clicked feature and create a copy of the coordinates array
const coordinates = e.features[0].geometry.coordinates.slice();
// Extract the city of the clicked feature for the popup content
const city = e.features[0].properties.city;
// Adjust coordinates if the map is zoomed out and the popup appears on a different copy of the feature
// This ensures the popup appears on the correct side of the map
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
// Check if there is already a popup on the map and if so, remove it to avoid multiple popups
if (popUps[0]) popUps[0].remove();
// Create and display the popup at the coordinates with the city
const popup = new mapboxgl.Popup(popupOptions)
.setLngLat(coordinates) // Set the longitude and latitude for the popup
.setHTML(city) // Set the HTML content of the popup
.addTo(map); // Add the popup to the map
// Add event listener to close card items when popup is closed
popup.on("close", () => {
locationMapCardClose(); // Close the detailed map card
console.log("popup was closed"); // Log that the popup was closed
});
};
Explanation:
- The
addPopup
function adds a popup to the map at the location of the clicked feature. - It adjusts the coordinates to ensure the popup appears correctly, removes any existing popups, and creates a new popup with the feature's city.
- An event listener is added to close the map card when the popup is closed.
Update Active Location in Sidebar
// Function to update the active item in the sidebar by adding the 'is--active' class
const updateActiveLocation = function (locationID) {
// Remove the 'is--active' class from all sidebar location items
locationItemSidebar.forEach((el) => el.classList.remove("is--active"));
// Loop through each sidebar location item to find the one with the matching locationID
locationItemSidebar.forEach((el) => {
// Check if the current item's data-id attribute matches the provided locationID
if (el.getAttribute("data-id") === locationID) {
// Add the 'is--active' class to the matching item
el.classList.add("is--active");
}
});
};
Explanation:
- The
updateActiveLocation
function updates the active item in the sidebar by adding theis--active
class to the item with the matchinglocationID
.
// Function to show the detailed map card for a specific location
const showMapCard = function (locationID) {
console.log(`Showing map card for location ID: ${locationID}`);
// Add the 'is--show' class to the map card wrapper to display it
locationMapCardWrapper.classList.add("is--show");
// Loop through each map card item and remove the 'is--show' class
locationMapCardItem.forEach((el) => {
el.classList.remove("is--show");
/*console.log(
`Removed 'is--show' class from card with data-id: ${el.getAttribute(
"data-id"
)}`
);*/
});
// Loop through each map card item to find the one with the matching locationID
locationMapCardItem.forEach((el) => {
/* console.log(
`Checking card with data-id: ${el.getAttribute(
"data-id"
)} against locationID: ${locationID}`
);*/
// Check if the current item's data-id attribute matches the provided locationID
if (el.getAttribute("data-id") === locationID) {
// Add the 'is--show' class to the matching item to display it
el.classList.add("is--show");
/*console.log(
`Added 'is--show' class to card with data-id: ${el.getAttribute(
"data-id"
)}`
);*/
}
});
};
Explanation:
- The
showMapCard
function displays the detailed map card for a specific location by adding theis--show
class to the map card wrapper and the matching map card item, while removing the class from all other items.
Fly to Location on the Map
// Fly to location on the map and zoom in - Adjust properties for different effects
const zoomToLocation = function (map, coordinates) {
map.flyTo({
center: coordinates, // Center the map on the provided coordinates
zoom: 14, // Set the zoom level to 14 for a closer view
speed: 1, // Set the animation speed (1 is default, higher is faster)
curve: 1, // Set the animation curve (1 is default, higher is more curved)
easing(t) {
return t; // Set the easing function for the animation (t is the current time)
},
});
};
Explanation:
- The
zoomToLocation
function zooms the map to a specific location using the provided coordinates, with adjustable properties for the zoom level, speed, curve, and easing function.
Event Listeners
These are the global event listeners for the project and a breakdown of what each does.
Click Event on Map Locations
//////////////////////////////////////////////////////////////
//////////////////// EVENT LISTENERS /////////////////////////
// Listens for clicks on the location layer of the map (dots on the map)
map.on("click", "locations", (e) => {
// Get the location ID from the clicked feature's properties
const locationID = e.features[0].properties.id;
// Log the location ID for debugging purposes
console.log(
`This is the location ID: ${locationID} that corresponds to the clicked feature.`
);
// Add a popup to the map at the location of the clicked feature
addPopup(e);
// Update the active location in the sidebar to highlight the clicked location
updateActiveLocation(locationID);
// Show the detailed map card for the clicked location
showMapCard(locationID);
// Zoom in to the clicked location on the map
zoomToLocation(map, e.features[0].geometry.coordinates);
});
Explanation:
- The
map.on("click", "locations", ...)
event listener listens for clicks on the location layer of the map (represented by dots). - When a location dot is clicked, it retrieves the
locationID
from the clicked feature's properties. - It logs the
locationID
for debugging, adds a popup to the map at the clicked location, updates the active location in the sidebar, shows the detailed map card, and zooms to the clicked location.
Cursor Style Change on Location Hover
// Changes cursor style when cursor moves onto the map layer "locations"
map.on("mouseenter", "locations", () => {
// Set cursor style to 'pointer' when hovering over a location dot
map.getCanvas().style.cursor = "pointer";
});
// Reverses cursor style when cursor moves off the map layer "locations"
map.on("mouseleave", "locations", () => {
// Reset cursor style when not hovering over a location dot
map.getCanvas().style.cursor = "";
});
Explanation:
- The
map.on("mouseenter", "locations", ...)
event listener changes the cursor style to a pointer when the cursor moves onto a location dot, indicating that the dot is clickable. - The
map.on("mouseleave", "locations", ...)
event listener resets the cursor style when the cursor moves off a location dot.
Close Map Card on Close Button Click
// Add event listener that closes the detailed card when the close button is clicked
locationMapCardCloseBtn.forEach((btn) => {
btn.addEventListener("click", () => {
// Call the function to close the detailed map card
locationMapCardClose();
});
});
Explanation:
- This code adds a click event listener to each close button of the map cards.
- When a close button is clicked, the
locationMapCardClose
function is called to close the detailed map card.
Click Event on Sidebar Location Items
// Add event listeners to the sidebar location items
locationItemSidebar.forEach((location) => {
// Add a click event listener to each sidebar location item
location.addEventListener("click", (e) => {
// Get the location ID from the data-id attribute of the clicked sidebar item
const locationID = e.currentTarget.getAttribute("data-id");
// Log the location ID for debugging purposes
console.log(
`This is the locationID: ${locationID} that corresponds to the clicked feature.`
);
// Check if the screen width is 767px or below (mobile view)
if (window.innerWidth <= 767) {
// Scroll to the section with ID 'section-map' to ensure the map is visible on mobile view
document.getElementById("section-map").scrollIntoView({
behavior: "smooth",
});
}
// Find the feature in the GeoJSON data that matches the location ID
const feature = stores.features.find(
(feature) => feature.properties.id === locationID
);
// If the feature is found in the GeoJSON data
if (feature) {
// Get the coordinates and city of the feature
const coordinates = feature.geometry.coordinates;
const city = feature.properties.city;
// Create a mock event object to pass to the addPopup function
const mockEvent = {
features: [
{
geometry: { coordinates: coordinates },
properties: { city: city },
},
],
lngLat: { lng: coordinates[0], lat: coordinates[1] },
};
// Add a popup at the feature's location
addPopup(mockEvent); This function is currently causing a bug - The screen is jumping around. It has to do with the close button on the popup being focused automatically. Will seek a solution and update, but for now, leave this commented out.
// Update the active location in the sidebar
updateActiveLocation(locationID);
// Show the corresponding map card
showMapCard(locationID);
// Zoom to the feature's location on the map
zoomToLocation(map, coordinates);
} else {
// Log an error message if the feature is not found in the GeoJSON data
console.error(`Feature with ID ${locationID} not found.`);
}
});
});
Explanation:
- This code adds click event listeners to each sidebar location item.
- When a sidebar location item is clicked, it retrieves the
locationID
from the data-id attribute of the clicked item. - It logs the
locationID
for debugging, checks if the screen width is 767px or below to scroll to the map section for mobile view, and finds the feature in the GeoJSON data that matches thelocationID
. - If the feature is found, it creates a mock event object to pass to the
addPopup
function, adds a popup at the feature's location, updates the active location in the sidebar, shows the corresponding map card, and zooms to the feature's location. - If the feature is not found, it logs an error message.
Documentation for Custom CSS for Map Integration
This section outlines the custom CSS used in the Webflow project to style the Mapbox map popups. These styles are added to the head section of the webpage with the map.
CSS Code
<style>
/* Style map popups */
.mapboxgl-popup-content {
pointer-events: auto; /* Allow pointer events for interactive content within the popup */
border-radius: 4px; /* Round the corners of the popup */
box-shadow: none; /* Remove any default shadow */
padding: 12px 16px; /* Add padding inside the popup for spacing */
color: var(--text-color--text-primary); /* Use the primary text color from the CSS variable */
background-color: #fcfcfc; /* Set the background color to a light shade */
font-weight: 500; /* Set the font weight to medium */
}
/* Popup bottom arrow color */
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
border-top-color: #fcfcfc; /* Match the arrow color to the popup background color */
}
</style>
Styling the Map Popups
.mapboxgl-popup-content {
pointer-events: auto;
border-radius: 4px;
box-shadow: none;
padding: 12px 16px;
color: var(--text-color--text-primary);
background-color: #fcfcfc;
font-weight: 500;
}
.mapboxgl-popup-content
:pointer-events: auto;
- Enables interactive content within the popup, such as links or buttons.border-radius: 4px;
- Rounds the corners of the popup for a smoother appearance.box-shadow: none;
- Removes any default shadow to keep the popup flat.padding: 12px 16px;
- Adds padding inside the popup for better spacing of the content.color: var(--text-color--text-primary);
- Sets the text color using a CSS variable, ensuring consistency with the overall site design.background-color: #fcfcfc;
- Sets a light background color for the popup to make it visually distinct.font-weight: 500;
- Uses a medium font weight for a balance between light and bold text.
Styling the Popup Arrow
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
border-top-color: #fcfcfc;
}
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip
:border-top-color: #fcfcfc;
- Matches the arrow color with the popup background to maintain a consistent visual appearance.
How to Integrate this CSS in Webflow
- Accessing the Page Settings:
- In your Webflow project, navigate to the page where you have integrated the Mapbox map.
- Adding Custom Code:
- Go to the page settings and scroll down to the Custom Code section.
- In the Head Code section, add the provided CSS code inside the
<style>
tags.
- Save and Publish:
- Save the changes and publish your site to see the updated styles in effect.
To ensure the Mapbox map functionality works as intended, the HTML elements need to be tagged with specific custom attributes. Here is a comprehensive list of all the HTML elements and their respective custom attributes used in the JavaScript code:
Custom Attributes and Their Purpose
- Map Container
- Element: The container element where the Mapbox map will be initialized.
- Attribute:
id="map"
- Example:
<div id="map"></div>
- Location Map Card Wrapper
- Element: The wrapper element for all the map card items.
- Attribute:
locations-map-card-wrapper
- Example:
<div locations-map-card-wrapper></div>
- Location Map Card Item
- Element: Each individual map card item element.
- Attribute:
locations-map-card-item
- Example:
<div locations-map-card-item></div>
- Location Map Card Close Button
- Element: The close button element for the map card items.
- Attribute:
locations-map-card-close-block
- Example:
<button locations-map-card-close-block>Close</button>
- Sidebar Location Item
- Element: Each location item in the sidebar.
- Attribute:
location-item-sidebar
- Example:
<div location-item-sidebar></div>
- Location List Sidebar
- Element: The parent element that contains all the sidebar location items.
- Attribute:
location-list-sidebar
- Example:
<div location-list-sidebar></div>
- Location Name Sidebar
- Element: The element containing the location name within each sidebar item.
- Attribute:
location-name-sidebar
- Example:
<div location-name-sidebar>Location Name</div>
- Location Latitude Sidebar
- Element: The element containing the latitude for the location.
- Attribute:
location-latitude-sidebar
- Example:
<div location-latitude-sidebar>32.771663</div>
- Location Longitude Sidebar
- Element: The element containing the longitude for the location.
- Attribute:
location-longitude-sidebar
- Example:
<div location-longitude-sidebar>-97.022553</div>
- Location Distance Sidebar
- Element: The element where the distance from the search result or user's location will be displayed.
- Attribute:
location-distance-sidebar
- Note: This element can be dynamically created if it does not already exist.
- Example:
<div location-distance-sidebar>1.00 miles</div>
Additional Notes
- Unique Identifiers: Ensure that each location item has a unique identifier (e.g., location name) that does not duplicate.
- Data Attributes: The
data-id
attributes are dynamically set in the JavaScript code to link sidebar items with map card items. - CSS Classes: The classes like
is--show
andis--active
are used to toggle visibility and active states in the JavaScript code.
Troubleshooting
Common Issues and How to Resolve Them
Issue: Map does not display correctly.
- Solution: Ensure that the Mapbox access token is correctly set and that the Mapbox GL JS and CSS files are properly included in the project. Verify the
container
ID in the Mapbox initialization matches the ID in your HTML.
Issue: Popups do not appear when clicking on map points.
- Solution: Check if the
addPopup
function is correctly called within the click event listener. Ensure that thepopUps
variable is correctly selecting existing popups and that the coordinates and description are properly set.
Issue: Sidebar items do not highlight or show the correct map card.
- Solution: Verify that the
data-id
attributes are correctly set on both the sidebar items and the map card items. Check theupdateActiveLocation
andshowMapCard
functions to ensure they are correctly identifying and updating elements.
Issue: Geolocation or search results are not updating the sidebar list.
- Solution: Ensure that the geolocation or search result event listeners are correctly calculating distances and sorting the sidebar list. Verify the logic in the distance calculation and the sorting functions.
Issue: Map points or sidebar items are not aligned with the GeoJSON data.
- Solution: Check the
getGeoData
function to ensure that the location elements are correctly converted to GeoJSON features and that thestores
object is accurately populated. Verify that thedata-id
attributes are correctly set.
Debugging Tips
- Use
console.log
Statements: Insertconsole.log
statements at key points in your code to track the flow of execution and the values of important variables.
console.log("GeoJSON data:", stores);
console.log("Clicked location ID:", locationID);
- Check Browser Console: Look for any error messages or warnings in the browser console that might give clues about what is going wrong.
- Verify HTML Structure: Use browser developer tools to inspect the HTML elements and ensure that they have the correct custom attributes and expected values.
References
Links to Relevant Documentation
- Mapbox GL JS Documentation: Mapbox GL JS
- Mapbox Geocoding API: Mapbox Geocoding
- Turf.js Documentation: Turf.js
Additional Resources or Tutorials for Further Learning
- Mapbox Tutorials: Mapbox Tutorials
- Geospatial Analysis with Turf.js: Turf.js Guide
- Responsive Web Design: MDN Web Docs - Responsive Design
- JavaScript Event Handling: MDN Web Docs - Event Handling
End to End Webflow Design and Development Services
From Web Design and SEO Optimization to Photography and Brand Strategy, we offer a range of services to cover all your digital marketing needs.
Webflow Web Design
We design custom Webflow websites that are unique, SEO optimized, and designed to convert.
Webflow Maintenance
Gain peace of mind knowing that a Webflow Professional Partner is maintaining your website.
Claim Your Design Spot Today
We dedicate our full attention and expertise to a select few projects each month, ensuring personalized service and results.