Пользовательский макет балуна с поддержкой автопозиционирования

Open in CodeSandbox

Для создания собственного балуна "с нуля" можно использовать фабрику templateLayoutFactory. При этом класс балуна должен реализовывать интерфейс IBalloon.

Чтобы балун поддерживал автопозиционирование, то есть автоматически подстраивался под размер содержимого, необходимо правильно оповещать о его текущих размерах. Для этого необходимо реализовать метод getShape().

При изменении контента балуна в методе onSublayoutSizeChange() следует переопределять опцию offset и кидать событие shapechange.

<!DOCTYPE html>
<html>
    <head>
        <title>
            Пользовательский макет балуна с поддержкой автопозиционирования
        </title>
        <meta
            http-equiv="Content-Type"
            content="text/html; charset=utf-8"
        />
        <link
            href="https://yandex.st/bootstrap/2.3.2/css/bootstrap.min.css"
            rel="stylesheet"
        />
        <script
            src="https://yandex.st/jquery/2.2.3/jquery.min.js"
            type="text/javascript"
        ></script>
        <!--
        Укажите свой API-ключ. Тестовый ключ НЕ БУДЕТ работать на других сайтах.
        Получить ключ можно в Кабинете разработчика: https://developer.tech.yandex.ru/keys/
    -->
        <script
            src="https://api-maps.yandex.ru/2.1/?lang=ru_RU&amp;apikey=<ваш API-ключ>"
            type="text/javascript"
        ></script>
        <script src="balloon_autopan.js" type="text/javascript"></script>
        <style>
            html,
            body {
                width: 100%;
                height: 100%;
                padding: 0;
                margin: 0;
            }
            #map {
                width: 100%;
                height: 85%;
            }
            .popover {
                display: block;
            }
            .popover .close {
                position: absolute;
                right: 5px;
                top: 1px;
            }
            .btn {
                margin-top: 10px;
            }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <button id="set-balloon-header" class="btn">
            Задать заголовок балуна
        </button>
        <button id="set-balloon-content" class="btn">
            Задать содержимое балуна
        </button>
    </body>
</html>
ymaps.ready(function () {
    // Создание экземпляра карты и его привязка к созданному контейнеру.
    var myMap = new ymaps.Map(
            "map",
            {
                center: [55.751574, 37.573856],
                zoom: 9,
                behaviors: ["default", "scrollZoom"],
            },
            {
                searchControlProvider: "yandex#search",
            }
        ),
        // Создание макета балуна на основе Twitter Bootstrap.
        MyBalloonLayout = ymaps.templateLayoutFactory.createClass(
            '<div class="popover top">' +
                '<a class="close" href="#">&times;</a>' +
                '<div class="arrow"></div>' +
                '<div class="popover-inner">' +
                "$[[options.contentLayout observeSize minWidth=235 maxWidth=235 maxHeight=350]]" +
                "</div>" +
                "</div>",
            {
                /**
                 * Строит экземпляр макета на основе шаблона и добавляет его в родительский HTML-элемент.
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/layout.templateBased.Base.xml#build
                 * @function
                 * @name build
                 */
                build: function () {
                    this.constructor.superclass.build.call(this);

                    this._$element = $(".popover", this.getParentElement());

                    this.applyElementOffset();

                    this._$element
                        .find(".close")
                        .on("click", $.proxy(this.onCloseClick, this));
                },

                /**
                 * Удаляет содержимое макета из DOM.
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/layout.templateBased.Base.xml#clear
                 * @function
                 * @name clear
                 */
                clear: function () {
                    this._$element.find(".close").off("click");

                    this.constructor.superclass.clear.call(this);
                },

                /**
                 * Метод будет вызван системой шаблонов АПИ при изменении размеров вложенного макета.
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/IBalloonLayout.xml#event-userclose
                 * @function
                 * @name onSublayoutSizeChange
                 */
                onSublayoutSizeChange: function () {
                    MyBalloonLayout.superclass.onSublayoutSizeChange.apply(
                        this,
                        arguments
                    );

                    if (!this._isElement(this._$element)) {
                        return;
                    }

                    this.applyElementOffset();

                    this.events.fire("shapechange");
                },

                /**
                 * Сдвигаем балун, чтобы "хвостик" указывал на точку привязки.
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/IBalloonLayout.xml#event-userclose
                 * @function
                 * @name applyElementOffset
                 */
                applyElementOffset: function () {
                    this._$element.css({
                        left: -(this._$element[0].offsetWidth / 2),
                        top: -(
                            this._$element[0].offsetHeight +
                            this._$element.find(".arrow")[0].offsetHeight
                        ),
                    });
                },

                /**
                 * Закрывает балун при клике на крестик, кидая событие "userclose" на макете.
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/IBalloonLayout.xml#event-userclose
                 * @function
                 * @name onCloseClick
                 */
                onCloseClick: function (e) {
                    e.preventDefault();

                    this.events.fire("userclose");
                },

                /**
                 * Используется для автопозиционирования (balloonAutoPan).
                 * @see https://api.yandex.ru/maps/doc/jsapi/2.1/ref/reference/ILayout.xml#getClientBounds
                 * @function
                 * @name getClientBounds
                 * @returns {Number[][]} Координаты левого верхнего и правого нижнего углов шаблона относительно точки привязки.
                 */
                getShape: function () {
                    if (!this._isElement(this._$element)) {
                        return MyBalloonLayout.superclass.getShape.call(
                            this
                        );
                    }

                    var position = this._$element.position();

                    return new ymaps.shape.Rectangle(
                        new ymaps.geometry.pixel.Rectangle([
                            [position.left, position.top],
                            [
                                position.left +
                                    this._$element[0].offsetWidth,
                                position.top +
                                    this._$element[0].offsetHeight +
                                    this._$element.find(".arrow")[0]
                                        .offsetHeight,
                            ],
                        ])
                    );
                },

                /**
                 * Проверяем наличие элемента (в ИЕ и Опере его еще может не быть).
                 * @function
                 * @private
                 * @name _isElement
                 * @param {jQuery} [element] Элемент.
                 * @returns {Boolean} Флаг наличия.
                 */
                _isElement: function (element) {
                    return (
                        element && element[0] && element.find(".arrow")[0]
                    );
                },
            }
        ),
        // Создание вложенного макета содержимого балуна.
        MyBalloonContentLayout = ymaps.templateLayoutFactory.createClass(
            '<h3 class="popover-title">$[properties.balloonHeader]</h3>' +
                '<div class="popover-content">$[properties.balloonContent]</div>'
        ),
        // Создание метки с пользовательским макетом балуна.
        myPlacemark = (window.myPlacemark = new ymaps.Placemark(
            myMap.getCenter(),
            {
                balloonHeader: "Заголовок балуна",
                balloonContent: "Контент балуна",
            },
            {
                balloonShadow: false,
                balloonLayout: MyBalloonLayout,
                balloonContentLayout: MyBalloonContentLayout,
                balloonPanelMaxMapArea: 0,
                // Не скрываем иконку при открытом балуне.
                // hideIconOnBalloonOpen: false,
                // И дополнительно смещаем балун, для открытия над иконкой.
                // balloonOffset: [3, -40]
            }
        ));

    myMap.geoObjects.add(myPlacemark);
});

$(function () {
    $("#set-balloon-header").click(function () {
        window.myPlacemark.properties.set(
            "balloonHeader",
            "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." +
                "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
        );
    });
    $("#set-balloon-content").click(function () {
        window.myPlacemark.properties.set(
            "balloonContent",
            "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." +
                "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
        );
    });
});