Webflow Tutorial

How to Add a MapBox Map to a Webflow Project - Complete Guide

Blog author Casey Lewis CL Creative
Casey Lewis
August 15, 2024
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 the stores 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:

  1. Setup Distance Calculation:
    • Set the units for distance calculation to miles using the options object.
  2. 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.
  3. Sort Locations by Distance:
    • Convert the list of locations to an array and sort them based on the distance attribute.
  4. 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.
  5. 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:

  1. Get the Closest Location:
    • Retrieve the first element from the sorted list of locations, which is the closest location.
  2. Highlight the Closest Location:
    • If a closest location is found, add the 'is--active' class to highlight it in the sidebar.
  3. Retrieve Location ID:
    • Get the ID of the closest location from its data-id attribute.
  4. Find the Feature in GeoJSON:
    • Search the GeoJSON data to find the feature that matches the location ID.
  5. Check if Feature Exists:
    • If the feature is found, proceed to extract its coordinates and city information.
  6. Create Mock Event for Popup:
    • Construct a mock event object that contains the feature's geometry and properties to pass to the addPopup function.
  7. 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.
  8. 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:

  1. 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.
  2. Add Geocoder Control:
    • map.addControl(geocoder): Adds the geocoder control to the map, allowing users to search for locations.
  3. Add Navigation Control:
    • map.addControl(new mapboxgl.NavigationControl()): Adds navigation controls (zoom and rotation) to the map for better user interaction.
  4. Event Listener for Geocoder Result:
    • geocoder.on("result", (event) => {...}): Adds an event listener that triggers when a search result is selected from the geocoder.
  5. Extract Search Result Geometry:
    • const searchResult = event.result.geometry: Extracts the geometry (coordinates) of the search result from the event object.
  6. 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.
  7. 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:

  1. 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.
  2. Add Geolocate Control:
    • map.addControl(geolocate): Adds the geolocate control to the map, allowing users to find their location.
  3. Event Listener for Geolocate Event:
    • geolocate.on("geolocate", (event) => {...}): Adds an event listener that triggers when the user's location is found.
  4. Log Geolocation Event:
    • console.log("A geolocate event has occurred."): Logs to the console that a geolocation event has occurred for debugging purposes.
  5. 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.
  6. 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.
  7. 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 the is--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 the is--active class to the item with the matching locationID.
  // 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 the is--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 the locationID.
  • 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

  1. Accessing the Page Settings:
    • In your Webflow project, navigate to the page where you have integrated the Mapbox map.
  2. 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.
  3. 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

  1. Map Container
    • Element: The container element where the Mapbox map will be initialized.
    • Attribute: id="map"
    • Example:
<div id="map"></div>
  1. 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>
  1. Location Map Card Item
    • Element: Each individual map card item element.
    • Attribute: locations-map-card-item
    • Example:
<div locations-map-card-item></div>
  1. 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>
  1. Sidebar Location Item
    • Element: Each location item in the sidebar.
    • Attribute: location-item-sidebar
    • Example:
<div location-item-sidebar></div>
  1. Location List Sidebar
    • Element: The parent element that contains all the sidebar location items.
    • Attribute: location-list-sidebar
    • Example:
<div location-list-sidebar></div>
  1. 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>
  1. Location Latitude Sidebar
    • Element: The element containing the latitude for the location.
    • Attribute: location-latitude-sidebar
    • Example:
<div location-latitude-sidebar>32.771663</div>
  1. Location Longitude Sidebar
    • Element: The element containing the longitude for the location.
    • Attribute: location-longitude-sidebar
    • Example:
<div location-longitude-sidebar>-97.022553</div>
  1. 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 and is--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 the popUps 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 the updateActiveLocation and showMapCard 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 the stores object is accurately populated. Verify that the data-id attributes are correctly set.

Debugging Tips

  • Use console.log Statements: Insert console.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

Additional Resources or Tutorials for Further Learning

Claim Your Design Spot Today

We dedicate our full attention and expertise to a select few projects each month, ensuring personalized service and results.

Web design portfolio