Делал на сайте Gstaadguy.
Слева города, справа показываем коордианты на карте.

Меняется когда листаем.

В админке для каждого города указываем ссылку с Apple Map:

Вот пример ссылки – https://maps.apple.com/place?place-id=I5A8FEE6D45BDBEA3&address=Boulevard+Jacqueline+Auriol+Inf%C3%A9rieur%2C+06200+Nice%2C+France&coordinate=43.6596058%2C7.2055078&name=%D0%B0%D1%8D%D1%80%D0%BE%D0%BF%D0%BE%D1%80%D1%82+%D0%9D%D0%B8%D1%86%D1%86%D0%B0+%D0%9B%D0%B0%D0%B7%D1%83%D1%80%D0%BD%D1%8B%D0%B9+%D0%91%D0%B5%D1%80%D0%B5%D0%B3&_provider=9902
Будем извлекать координаты в JS по слову “coordinate“.
HTML разметка.
Города:
$address = get_field('address', $post->ID);
$map_url = $address['google_maps_url'] ?? '';
....
<div class="section experience-item experience--js experience-map--js" id="<?php echo slugify($slug) ?>" data-travel-name="<?php echo $post_title?>" data-map-location="<?php echo $map_url?>">
<div class="experience-item__box">
<div class="container--narrow">
<?php render_block_title($label, $title, $title_tag, $content); ?>
<?php if ($list) : ?>
<div class="experience-item__list-wrapper">
<?php if ($list_header) : ?>
<div class="experience-item__list-header">
<?php echo $list_header; ?>
</div>
<?php endif; ?>
<ul class="experience-item__list">
<?php foreach ($list as $list_item) :
$label = $list_item['label'];
$content = $list_item['content'];
?>
<li class="experience-item__list-item body-s">
<div class="experience-item__list-label body-s-bold">
<?php echo $label; ?>
</div>
<div class="experience-item__list-content">
<?php echo $content; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($pro_tip_content) : ?>
<div class="experience-item__pro-tip">
<?php if ($pro_tip_label) : ?>
<div class="experience-item__pro-tip-label body-xs">
<?php echo $pro_tip_label; ?>
</div>
<?php endif; ?>
<h5 class="experience-item__pro-tip-content">
<?php echo $pro_tip_content; ?>
</h5>
</div>
<?php endif; ?>
<?php if ($button && !empty($button['button']['link']['url']) && !empty($button['button']['link']['title'])): ?>
<div class="experience-item__btn">
<?php
$link = $button['button']['link'];
$url = esc_url($link['url']);
$title = esc_html($link['title']);
$target = !empty($link['target']) ? esc_attr($link['target']) : '_self';
?>
<?php if ($button_type === 'button--opentable'): ?>
<a href="<?php echo $url; ?>" target="<?php echo $target; ?>" class="button button--secondary <?php echo esc_attr($button_type); ?>">
<?php echo $title; ?>
<?php echo get_inline_svg('OpentableLogo.svg'); ?>
</a>
<?php else: ?>
<a href="<?php echo $url; ?>" target="<?php echo $target; ?>" class="button button--primary <?php echo esc_attr($button_type); ?>">
<?php echo $title; ?>
<?php echo get_inline_svg('whatsapp.svg'); ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($gallery): ?>
<?php $is_disabled = count($gallery) < 4 ? 'slider--disabled' : ''; ?>
<div class="experience-item__slider">
<div class="container-slider">
<div class="experience-item__slider-wrapper js-default-slider-wrapper lightgallery--js <?php echo $is_disabled; ?>">
<ul class="js-default-slider">
<?php foreach ($gallery as $image): ?>
<li>
<?php if (!empty($image['ID'])): ?>
<img
class="lightgallery-item"
<?php awesome_acf_responsive_image(
$image['ID'],
'experience-gallery',
'rectangle-rotated',
300,
555
); ?>
alt="<?php echo esc_attr($image['alt']); ?>"
>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<div class="js-default-nav-prev">
<?php echo get_inline_svg('ArrowSlider.svg') ?>
</div>
<div class="js-default-nav-next">
<?php echo get_inline_svg('ArrowSlider.svg') ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
Ключевое для нас:
- data-map-location=”<?php echo $map_url?>”
- experience-map–js
Разметка контейнера:
<div class="single-guide js-map-wrapper" data-latitude="51.5074" data-longitude="-0.1278">
<div class="single-guide__row">
<div class="single-guide__col">
<?php
get_template_part('template-parts/acf-loop');
?>
</div>
<div class="single-guide__col js-sticky-wrapper">
<div id="map" class="sidebar map js-sticky-el js-map">
</div>
</div>
</div>
</div>
- data-latitude=”51.5074″ data-longitude=”-0.1278″ – начальные координаты карты. Я потом в JS переопределяю, ставлю координаты первого города.
- Навешиваем классы .js-map-wrapper, js-sticky-wrapper, map js-sticky-el js-map
- Навешиваем id=”map”
JavaScript.
Подключаем через npm gsap и в head подключаем MapKit JS.
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width initial-scale=1">
<script
src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"
crossorigin async
></script>
Скрипт.
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const map = () => {
const JWT_TOKEN = codelibry.map_token;
if (!JWT_TOKEN) return;
const CLASSNAMES = {
wrapper: '.js-map-wrapper',
map: '.js-map',
location: '[data-map-location]',
stickyEl: '.js-sticky-el',
stickyWrapper: '.js-sticky-wrapper',
experienceItem: '.experience-map--js',
};
mapkit.init({
authorizationCallback: function (done) {
done(JWT_TOKEN);
},
});
const $wrapper = document.querySelector(CLASSNAMES.wrapper);
if (!$wrapper) return;
const $map = document.querySelector(CLASSNAMES.map);
const $locations = document.querySelectorAll(CLASSNAMES.location);
const $stickyWrapper = document.querySelector(CLASSNAMES.stickyWrapper);
const $stickyEl = document.querySelector(CLASSNAMES.stickyEl);
const offsetTop = 100;
const $experienceItem = document.querySelector(CLASSNAMES.experienceItem);
let initialCoords = {
latitude: Number($wrapper.dataset.latitude),
longitude: Number($wrapper.dataset.longitude),
};
if ($experienceItem?.dataset.mapLocation) {
const rawCoords = $experienceItem.dataset.mapLocation;
const parsedCoords = getCoordinatesFromUrl(rawCoords);
if (parsedCoords?.latitude && parsedCoords?.longitude) {
initialCoords = parsedCoords;
}
}
const mapInit = new mapkit.Map($map, {
region: new mapkit.CoordinateRegion(
new mapkit.Coordinate(initialCoords.latitude, initialCoords.longitude),
new mapkit.CoordinateSpan(0.2, 0.2),
),
});
const markersMap = new Map();
function getCoordinatesFromUrl(url) {
if (!url || typeof url !== 'string' || !url.includes('coordinate=')) {
return null;
}
const coordinatePart = url.split('coordinate=')[1];
if (!coordinatePart || !coordinatePart.includes('%2C')) {
return null;
}
const coordinates = coordinatePart.split('%2C');
const latitude = parseFloat(coordinates[0]);
const longitude = parseFloat(coordinates[1]);
if (isNaN(latitude) || isNaN(longitude)) {
return null;
}
return { latitude, longitude };
}
// this func find all data from el and
// add all marker on the map one time
function initMarkers() {
markersMap.forEach((data) => {
mapInit.removeAnnotation(data.marker);
});
markersMap.clear();
$locations.forEach(($location) => {
const itemUrl = $location.getAttribute('data-map-location');
const coords = getCoordinatesFromUrl(itemUrl);
// ⛔ Пропускаємо, якщо координати некоректні або відсутні
if (!coords) {
console.warn('Пропущено елемент без координат:', $location);
return;
}
const coordinate = new mapkit.Coordinate(coords.latitude, coords.longitude);
const locationId = $location.getAttribute('id');
const marker = new mapkit.MarkerAnnotation(coordinate, {
title: 'Location',
subtitle: $location.textContent,
color: '#FF5733',
glyphText: '!',
selected: false,
callout: {
enabled: true,
calloutContentForAnnotation: () => {
const div = document.createElement('div');
div.innerHTML = `
<a class="map__marker_link" href="#${locationId}"
style="color: blue; text-decoration: underline;">
${$location.textContent}
</a>
`;
return div;
},
},
});
mapInit.addAnnotation(marker);
markersMap.set(locationId, {
marker: marker,
coordinate: coordinate,
});
});
}
// sticky animation
function initStickyAnimation() {
ScrollTrigger.create({
id: 'sticky-animation',
trigger: $stickyWrapper,
start: `top-=${offsetTop}px top`,
end: () => `bottom ${$stickyEl.offsetHeight + offsetTop}px`,
pin: $stickyEl,
pinSpacing: false,
markers: true,
invalidateOnRefresh: true,
});
}
// trigger callback
function initLocationTriggers() {
$locations.forEach(($location) => {
ScrollTrigger.create({
trigger: $location,
start: 'center center',
onEnter: () => handleLocationTrigger($location),
onEnterBack: () => handleLocationTrigger($location),
});
});
}
function handleLocationTrigger($location) {
const locationId = $location.getAttribute('id');
const markerData = markersMap.get(locationId);
if (!markerData) return;
// map zoom
const zoom = 0.004;
const region = new mapkit.CoordinateRegion(markerData.coordinate, new mapkit.CoordinateSpan(zoom, zoom));
// add color to active marker
markerData.marker.selected = true;
markerData.marker.color = '#5F0100';
// remove active color
markersMap.forEach((data, id) => {
if (id !== locationId) {
data.marker.selected = false;
data.marker.color = '#FF5733';
}
});
// animation when map scroll to marker
mapInit.setRegionAnimated(region, {
animate: true,
duration: 1.0,
});
}
let mm = gsap.matchMedia();
mm.add('(min-width: 800px)', () => {
ScrollTrigger.refresh();
initMarkers();
initStickyAnimation();
initLocationTriggers();
});
mm.add('(max-width: 799px)', () => {
});
window.addEventListener('resize', () => {
ScrollTrigger.refresh();
});
};
export default map;
Ініціалізація карти
Мапа створюється з центром у координатах, які взяті з .js-map-wrapper або з першого .experience-map--js, якщо в нього є data-map-location.
Функція getCoordinatesFromUrl()
Розбирає координати з URL і повертає { latitude, longitude }. Якщо координати неправильні — повертає null.
Функція initMarkers()
- Видаляє попередні маркери (очищення мапи).
- Проходиться по всіх елементах з
data-map-location. - Для кожного створює маркер на мапі.
- Пропускає елементи без координат (із
console.warnдля відладки).
Sticky мапа (анімована при скролі)
ScrollTrigger.create({
id: 'sticky-animation',
trigger: $stickyWrapper,
start: `top-=${offsetTop}px top`,
end: () => `bottom ${$stickyEl.offsetHeight + offsetTop}px`,
pin: $stickyEl,
});
Елемент .js-sticky-el залишається прикріпленим до екрану, поки користувач скролить список .experience-item.
Фокусування на маркер при скролі
function handleLocationTrigger($location) { ... }
Коли користувач доскролює до певного .experience-item, мапа:
- автоматично масштабується до відповідного маркера,
- виділяє його кольором,
- скидає кольори інших маркерів.
GSAP MatchMedia
mm.add('(min-width: 800px)', () => { ... });
Карта, маркери та ScrollTrigger активуються лише для десктопів (≥800px ширина). На мобільних ця логіка вимикається.
