Яндекс.Карты. Меняем метку при наведении нестандартным способом расширяя содержимое метки контентом!

Делюсь наработкой, на которую потратил несколько часов пока бился над решением проблемы. Может кому пригодится, да и для себя чтобы не забыть (пока память свежа изложить полученные знания в шпаргалку).
Понадобилось реализовать поведение карты чтобы при наведении на метку к метке справа добавлялся свой блок с содержимым (так называемый iconContentLayout). Пример:

Не нашел нигде как сделать так. В официальной документации было только примеры изменения иконки метки через замену iconImageHref (аля «Поменять иконку при наведении на точку на яндекс карте»). Но мне не надо было менять иконку, а надо было вывести (прицепить) дополнительный блок iconContentLayout. Если его по умолчанию выводить то будет некрасиво и завал всей карты как тут: https://dev445.gowindo.ru/dealers:

Согласитесь, это некрасиво. По задумке дизайнера макета должно было быть так чтобы блок справа от метки всегда выводился. Но когда меток стало очень много, получилось как выше. Разумеется нам так не надо.
И решением стало следующее — появление допблока от метки справа при наведении мышки на метку:


Для этого сделал следующее:
не указываю iconContentLayout по умолчанию. Но содержимое для нее задаю в переменной MyIconContentLayout. Затем добавляю события наведения на иконку mouseenter и потери фокуса мышки mouseleave.
Чтобы понять как называется свойство/переменная или что надо менять использовал вывод в консоль содержимого объекта e.get('target').options (ниже в коде) — так понял что нужна команда при наведении
e.get('target').options.set('iconContentLayout', MyIconContentLayout);
и при потере фокуса мыши соответственно
e.get('target').options.set('iconContentLayout', '');
И вуаля, все заработало.

Ниже код блока карты целиком, пример выложил тут.

В коде также закомментировать метод добавления большого количества меток адресов с помощью objectManager, но у меня не получилось заставить поменять иконку метки. Потому использовал обычный метод добавления с помощью myPlacemarkWithContent.

Также сохранил тут пример как получить сведения об организации, может кому пригодится. Вроде как это платная опция у Яндекса, но я нашел недокументированный метод получения сведений об организации и способ вытащить сведения об оценке организации и отзывах, времени работы и т.д.

<!-- Yandex Map подключаем с ключами api key JavaScript API и HTTP Геокодер и API Геосаджеста которые подключаем в https://developer.tech.yandex.ru/services -->
<script src="https://api-maps.yandex.ru/2.1/?apikey={'ymapapikey'|config}&suggest_apikey={'ymapgeosadjestapikey'|config}&lang=ru_RU&load=package.full&ns=ymaps{*&onload=mscDistance.Ymaps.ready&mode=debug*}" type="text/javascript">
}
</script>
{*Создаем системную настройку или лучше в Конфигурации настройку ymapapikey и ymapgeosadjestapikey с соответствующими ключами*}
<!-- Yandex Map -->
{*Статистику вызовов можно посмотреть в https://developer.tech.yandex.ru/services/3/stat/ c аккаунта на котором получали APi-ключи*}
{*Координаты центрирования и метки на карте задаются в настройках конфигурации    *}
{*ОБЯЗАТЕЛЬНО! для кода скриптов javascript до и после фигурных скобок {} ставить пробел, для кода fenom без пробелов! Иначе вызовет ошибку и сайт может не открываться! *}
{set $ymaps_center_latitude = 'ymap_center_latitude'|config}
{set $ymaps_center_longitude = 'ymap_center_longitude'|config}

{set $dealers_on_cart = json_decode($_modx->resource.id | resource : 'dealers_on_cart' , true)}
{set $zvezdochka = '<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 0.5L7.14874 3.73071L10.7308 4.30041L8.16771 6.86679L8.73282 10.4496L5.5 8.805L2.26718 10.4496L2.83229 6.86679L0.269189 4.30041L3.85126 3.73071L5.5 0.5Z" fill="#FFE88A"/></svg>'}
{set $logokartochkidefault= 'assets/images/logo/logo-longteng-71-71.svg'}
  <section class="map">
    <div class="container ">
      <div class="row mx-0 mb-4 py-1">
        <div class="col-12 px-0 map" id="map">
            <script>
                ymaps.ready(init); 
                function init() {  
                    var myMap = new ymaps.Map('map', {
                            center: [{$ymaps_center_latitude},{$ymaps_center_longitude}], {*задаем по координатам указываем в Конфигурации, чтобы клиент потом мог править центрирование*}
                            //center: [55.168000,83.083361], {* или так напрямую*}
                            zoom: 4.4, {* Я бы zoom тоже вынес в настройки конфигурации*}
                            controls: []
                        }, {
                            //searchControlProvider: 'yandex#search'
                        }),
                        objectManager = new ymaps.ObjectManager({
                            // Чтобы метки начали кластеризоваться, выставляем опцию.
                            clusterize: true,
                            // ObjectManager принимает те же опции, что и кластеризатор.
                            gridSize: 32,
                            clusterDisableClickZoom: true
                        });
                    // Чтобы задать опции одиночным объектам и кластерам,
                    // обратимся к дочерним коллекциям ObjectManager.
                     var objectManager = new ymaps.ObjectManager({ clusterize: true });
                     var  currentId = 0;
                   {foreach $dealers_on_cart as $idx =>$dealerplace index=$index}
                        {if $dealerplace.published}
                    {*смотри https://yandex.ru/dev/jsapi-v2-1/doc/ru/v2-1/ref/reference/ObjectManager#add - добавление коллекции*}
                    {*1-й метод добавляет метку но не понятно как с помощью objectManager вывести нужные иконки*}
                    {*создаем массив данных для objectManager*}
                    {*смотри https://yandex.ru/dev/maps/jsbox/2.1/object_manager*}
{*                                        objectManager.add( { 
                                type: 'Feature',
                                id: currentId++,
                                geometry: {
                                    type: 'Point',
                                    coordinates: [{$dealerplace.ymaps_placemark_latitude},{$dealerplace.ymaps_placemark_longitude}]
                                },
                                 properties: {
                                     balloonContentHeader: '<div class="d-flex justify-content-between balheader"><div><div><a target="_blank" href="{$dealerplace.url}" class="bal-zagolovok">{$dealerplace.name}</a>
{if $dealerplace.ratting}<span class="bal-description">{$zvezdochka} {$dealerplace.ratting}</span><span class="bal-description-grey">({$dealerplace.callbackscount})</span>{/if} </div><div class="bal-description-2str"> {if $dealerplace.workdaytimeend}Открыто до {$dealerplace.workdaytimeend}{/if}
{$dealerplace.address}</div></div> <div><img src="{$logokartochkidefault}" style="max-width: 150px;max-height: 70px;margin-left:10px;"></div></div>', 
                                     balloonContentBody: '<div class="balbody"></div>',
                                     balloonContentFooter: '<div class="d-flex justify-content-start balfooter"><a href="https://yandex.ru/maps/?rtext=~{$dealerplace.ymaps_placemark_latitude},{$dealerplace.ymaps_placemark_longitude}""  target="_blank" class="btn  btn-primary btn-marshrut " onclick="">Маршрут</a> <a href="{$dealerplace.url}" class="btn  btn-primary btn-pereiti ms-2">Перейти на сайт</a></div>', 
                                     clusterCaption: '{$dealerplace.name}', 
                                     hintContent: '<strong>{$dealerplace.name} </strong> {$dealerplace.address}'
                                } 
                            } );
                            var myPlacemark = new ymaps.Placemark([{$dealerplace.ymaps_placemark_latitude},{$dealerplace.ymaps_placemark_longitude}], {}, {
                                iconLayout: 'default#image', 
                                iconImageHref: 'assets/images/icons/placemark2.svg',
                                iconImageSize: [28, 34],
                                iconImageOffset: [-10, -28]
                            });
                            myMap.geoObjects.add(myPlacemark);
*}
                        {*2-й метод  с добавлением иконки метки*}
                            	// Создаём макет содержимого.
                            	{*что такое iconlayoutlg">$[properties.iconContent] - это не понял что всвтавляет но и не убрал, может пригодится потом разобраться как это добавляется и что это?*}
                            MyIconContentLayout{$dealerplace.MIGX_id} = ymaps.templateLayoutFactory.createClass(
                                '<div class="iconlayoutlg">$[properties.iconContent] <div class=""><div class="d-flex justify-content-between ps-4"><div>{$dealerplace.name} </div> {if $dealerplace.ratting}<div class="px-2"><span class="bal-description">{$zvezdochka} {$dealerplace.ratting}</span><span class="bal-description-grey">({$dealerplace.callbackscount})</span></div>{/if}</div> <div class="text-start ps-4"> Открыто до 18:00</div></div> </div>'
                            );
                            var myPlacemarkWithContent = new ymaps.Placemark([{$dealerplace.ymaps_placemark_latitude},{$dealerplace.ymaps_placemark_longitude}],
                        	    { 
                                    // Зададим содержимое заголовка балуна.
                                    balloonContentHeader:  '<div class="d-flex justify-content-between balheader"><div><div><a target="_blank" href="{$dealerplace.url}" class="bal-zagolovok">{$dealerplace.name}</a>
{if $dealerplace.ratting}<span class="bal-description">{$zvezdochka} {$dealerplace.ratting}</span><span class="bal-description-grey">({$dealerplace.callbackscount})</span>{/if} </div><div class="bal-description-2str"> {if $dealerplace.workdaytimeend}Открыто до {$dealerplace.workdaytimeend}{/if}
{$dealerplace.address}</div></div> <div><img src="{$logokartochkidefault}" style="max-width: 150px;max-height: 70px;margin-left:10px;"></div></div>',
                                    // Зададим содержимое основной части балуна.
                                    balloonContentBody: '<div class="balbody"></div>',
                                    // Зададим содержимое нижней части балуна.
                                    balloonContentFooter: '<div class="d-flex justify-content-start balfooter"><a href="https://yandex.ru/maps/?rtext=~{$dealerplace.ymaps_placemark_latitude},{$dealerplace.ymaps_placemark_longitude}""  target="_blank" class="btn  btn-primary btn-marshrut " onclick="">Маршрут</a> <a href="{$dealerplace.url}" class="btn  btn-primary btn-pereiti ms-2">Перейти на сайт</a></div>',
                                    // Зададим содержимое всплывающей подсказки.
                                    hintContent: '<strong class="hintcontentlg">{$dealerplace.address} </strong> '
                                } , {
                                    // Опции.
                                    // Необходимо указать данный тип макета.
                                    iconLayout: 'default#imageWithContent',
                                    // Своё изображение иконки метки.
                                    iconImageHref: 'assets/images/icons/placemark2.svg',
                                    // Размеры метки.
                                    iconImageSize: [28, 34], 
                                    // Смещение левого верхнего угла иконки относительно
                                    // её "ножки" (точки привязки).
                                    iconImageOffset: [-10, -28],
                                    // Смещение слоя с содержимым относительно слоя с картинкой.
                                    iconContentOffset: [0, 0],
                                    // Макет содержимого.
                                    //iconContentLayout: MyIconContentLayout{$dealerplace.MIGX_id}
                                } 
                            ); 
                        // Добавим метку на карту.
                        myMap.geoObjects.add(myPlacemarkWithContent);
                        {*Сделал! СДЕЛАЛЛЛЛЛЛ! Короче при наведении мышки на метку добавляет содержимое метки, при отведении мышки с метки отключает содержимое. По макету была длинная метка - если ставить iconContentLayout: MyIconContentLayout сразу по умолчанию, то карта просто становится забитой большими широкими метками. А так аккуратненько, показываем iconContentLayout при наведении. Нигде нет такого примера, самому пришлось разработать. *}
                        myPlacemarkWithContent.events
                        .add('mouseenter', function (e) {
                          e.get('target').options.set('iconContentLayout', MyIconContentLayout{$dealerplace.MIGX_id});
							{*раскомментируйте если хотите в лог вывести информацию для анализа данных*}
                        {*  {if $_modx->user.id == 1} *} {*тут вывожу в лог объекты для анализа и тестирования, только для админа 1 - потому тут не должно быть пробелов после открывающей "{" и перед закрывающей "}" *}
                            {*  console.group("mouseenter"); *}
                            {*  console.log(e.get('target').options); *} {*смотрим содержимое с чем можно поработать! *}
                            {*  console.log(e.get('target')); *}
                            {*  console.groupEnd(); *}
						{* {/if} *}

                           } )
                        .add('mouseleave', function (e) { 
                          e.get('target').options.set('iconContentLayout', '');
						  {*раскомментируйте (удалите фигурные скобки со звездочкой вокруг команд)если хотите в лог вывести информацию для анализа данных*}
						  {*{if $_modx->user.id == 1}*} {*тут вывожу в лог объекты для анализа и тестирования, только для админа 1 - потому тут не должно быть пробелов после открывающей "{" и перед закрывающей "}" *}
							{*console.group("mouseleave");*}
                            {*console.log(e.get('target').options);*} {*смотрим содержимое с чем можно поработать!*}
                            {*console.log(e.get('target')); *}
                            {*console.groupEnd();*}
						{*{/if} *}
                          } );
                        {/if}
                    {/foreach}
                    {*метод добавляет точку но не выводит почему то всплывающие формы*}
                    objectManager.objects.options.set('preset', 'islands#greenDotIcon');
                    objectManager.clusters.options.set('preset', 'islands#greenClusterIcons');
                    myMap.geoObjects.add(objectManager);
{*метод поиска информации о организации и выводе в карточку для себя - по примеру можно вытащить рейтинг и отзывы, время работы. пока отключил, т.к. заказчик не захотел светить плохие оценки :)*}
                    {*РАБОТАЕТ! ИСПОЛЬЗОВАТЬ В ДАЛЬНЕЙШЕМ! Крутая фича! Нашел супер способ вытащить данные по организации! https://ru.stackoverflow.com/questions/1129875/%D0%9A%D0%B0%D0%BA-%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D1%82%D1%8C-%D1%80%D0%B5%D0%B9%D1%82%D0%B8%D0%BD%D0%B3-%D0%BA%D0%BE%D0%BC%D0%BF%D0%B0%D0%BD%D0%B8%D0%B8-%D1%81-%D1%8F%D0%BD%D0%B4%D0%B5%D0%BA%D1%81-%D0%BA%D0%B0%D1%80%D1%82%D1%8B
                    Вообще эта функция недокументирована,может быть отключена в будущем, т.к. Яндекс сделал платным доступ к информации по организациям!*}
					{*тут 86592472630 - номер  организации из справочника Яндекс.Карт, задаете поиск организации и смотрите url - там будет номер, потом можно программмно подставлять нужные номера. Я бы номера организации и дилеров записывал в поле данных MIGx*}
{*                     ymaps.findOrganization('86592472630').then(
                        function (orgGeoObject) {
                        console.log('Структура данных огранизации:');
                        console.log(orgGeoObject.properties);
                        console.log('-------------------------------');
                        
                        myMap.geoObjects.add(orgGeoObject);
                       // orgGeoObject.balloon.open();
                        var rating = orgGeoObject.properties.get('rating');
                        console.log('Рейтинг:');
                        console.log('Оценок: ' + rating.ratings);
                        console.log('Обзоров: ' + rating.reviews);
                        console.log('Балл: ' + rating.score);
                        }
                    );
*}
{*метод поиска *}                           
                } ;       

            </script>
        </div>
      </div>
    </div>
  </section>
  {*далее стили настраиваете под себя*}
<style>
    .btn-marshrut {
        padding: 10px;
        width: 82px;
        height: 36px;
        background: #196DFF;
        border-radius: 11px;
        font-family:Inter;
        font-size: 13px!important;
        font-weight: 700;
        line-height: 16.25px!important;
        text-align: left;
    }
    .btn-pereiti {
        
        height: 36px;
        background: #3CB200;
        border-radius: 11px;
        font-family: Inter;
        font-size: 13px !important;
        font-weight: 700;
        line-height: 16.25px !important;
        text-align: left;
        border:0;
    }
    .balheader {
        margin-top: 5px;
        margin-left: 3px;
    }
    .balbody {
        padding-bottom: 20px;
        margin-left: 3px;
    }
    .balfooter {
        padding-bottom:2px;
    }
    .bal-zagolovok {
        width: 115px;
        height: 18px;
        top: 15px;
        left: 15px;
        gap: 0px;
        opacity: 0px;
        padding-bottom:7px;

    }
    .bal-description-grey {
    color: #4D4D4D;
    
    }
    .bal-description {
        
    }
    .bal-description, .bal-description-grey {
        font-family: Inter;
        font-size: 14px;
        font-weight: 600;
        line-height: 17.5px;
        text-align: left;
        }
    .bal-description-2str   {
        font-family: Inter;
        font-size: 13px;
        font-weight: 500;
        line-height: 16.25px;
        text-align: left;
    }
    .ymaps-2-1-79-balloon__content {
        border-radius:7px;
    }
    .ymaps-2-1-79-balloon__layout {
        border-radius:7px;
    }
    .ymaps-2-1-79-balloon__close-button {
        border-radius:7px;
    }
    .iconlayoutlg {
        color: #000;
        font-weight: bold;
        position: absolute;
        top: 0px;left: 14px;
        height: 36px;
        align-content: center;
        z-index: -1;
        background: #FFFFFF;
        border-radius: 10px 10px 10px 0;
        white-space: nowrap;
    }
</style>
Олег Захаров
05 апреля 2024, 02:54
modx.pro
3
1 724
+8

Комментарии: 6

Дима Касаткин
06 апреля 2024, 22:07
+1
Благодарю за полезный материал!

Для тех, кто захочет воспользоваться решением, рекомендую заменить JS-комментарии (/* такие */) на fenom-комментарии ( {*на такие*} ) чтобы на фронтенд не выводить их!
    Олег Захаров
    06 апреля 2024, 22:31
    +1
    да правильно, поправил в ТС код.
    Пример работы выложил тут https://dev445.gowindo.ru/dealers
    Есть одна недоработка. Когда нажимаешь на метку, раскрывается всплывающая карточка организации (balloonContent). После закрытия карточки выведенный справа блок от метки iconContentLayout остается открытым (т.к. мышка оказывается за пределами границ иконки геометки myPlacemarkWithContent после закрытия balloonContent, но событие mouseleave не срабатывает). И показанный блок iconContentLayout не закрывается пока снова не наведешь мышку на метку и выйдешь за пределы метки (повторно вызвав срабатывание mouseenter и mouseleave). Надо бы доделать, повесить вызов кода либо событие на закрытие balloonContent, либо добавить событие потери фокуса после попадания мышки за пределы границ иконки геометки myPlacemarkWithContent. Пока лень думать над этим, т.к. пока Заказчику это не горит.
    Илья
    08 апреля 2024, 10:51
    0
    Вот если бы кто подружил ms2Gallery и Яндекс.Карты чтобы в балун фотографии поддягивать.
      Misha Bulic
      08 апреля 2024, 11:56
      0
      А какая проблема?
        Илья
        08 апреля 2024, 14:35
        0
        Я использую YandexMaps для вывода ресурсов на карте. Вывести изображение ресурса в балун просто в плейсхолдер типа {$medium} не возможно.
        modx.pro/help/9334
        Олег Захаров
        12 апреля 2024, 11:28
        0
        Поправил код выше. Столкнулся с тем что у меток выводился одинаковое содержимое для прикрепляемого справа от метки блока. Понял что по причине того что в карту выводится одна переменная MyIconContentLayout, а для разных меток она разная.
        Исправил: вместо присвоения
        MyIconContentLayout = ymaps.templateLayoutFactory.createClass(...
        поставил
        MyIconContentLayout{$dealerplace.MIGX_id} = ymaps.templateLayoutFactory.createClass(...
        и вместо
        e.get('target').options.set('iconContentLayout', MyIconContentLayout);
        поставил
        e.get('target').options.set('iconContentLayout', MyIconContentLayout{$dealerplace.MIGX_id});
        Как итог будет правильно выводить текст справа от меток.
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        6