[JavaScript]ターゲット要素の移動に連動した吹き出しDOMの作成(レスポンシブ対応)

特定の要素の下に矢印付きの吹き出しを表示する。要件は下記のようにまとめた。

  • 特定の要素の下から吹き出しが出る
  • 吹き出しの上には矢印がある
  • 吹き出しにつく矢印は常に特定の要素の中央から表示される。
  • 特定の要素がWindow幅などで移動した場合、吹き出しは追従する。(レスポンシブ対応)

完成イメージはこのような形。

目次

動的な吹き出しを実装する

特定の要素に追従する吹き出しを実装する。

HTMLで要素を作る

まずはHTMLを考える。
特定の要素(矢印を追従させたい要素)にid=”target”をつけて特定できるようにする。
吹き出し要素はid=”balloon”とした。吹き出しのHTMLはシンプルで、class=”balloon-content”をもつdivがテキスト要素、class=”balloon-arrow”をもつdivを矢印要素としている。

<body>
    <div class="box">ターゲットのテキスト「<span id="target">X</span>」</div>
    <div id="balloon" class="balloon">
        <div class="balloon-content">吹き出しの表示エリア。吹き出しの表示エリア。</div>
        <div class="balloon-arrow"></div>
    </div>
</body>

CSSで装飾する

CSSでバルーンを装飾する。
特筆すべき点はないが、.balloon-arrowクラスの矢印はborderプロパティで作成している。

    body {
        height: 100vh;
    }

    .box {
        width: fit-content;
        padding: 8px 16px;
        background-color: #3b3b3b;
        color: #fff;
        position: relative;
        text-wrap: wrap;
        display: inline-block;
    }

    .balloon {
        position: absolute;
        background-color: #007bff;
        color: #fff;
        padding: 10px;
        border-radius: 5px;
        display: none;
        max-width: 320px;
        z-index: 1000;
    }

    .balloon-arrow {
        position: absolute;
        width: 0;
        height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid #007bff;
        top: -8px;
        transform: translateX(-50%) rotate(180deg);
    }

    .balloon-content {
        font-size: 14px;
    }

JavaScriptで動的に吹き出し位置を変更する

JavaScriptでレスポンシブの時にも矢印や吹き出しの位置が動的に変更できるようにする。

getOverlayPosition関数で吹き出し位置を返す

getOverlayPosition関数を作成する。
ここでは、target要素に対する吹き出しの位置を計算して、Objectで吹き出しの上、左の位置、ターゲット要素の水平方向の中心(吹き出しの矢印で使う。)を返すようにした。

HTMLElementでターゲット要素と吹き出し要素を渡す。
そして、この関数の中で、getBoundingClientRectにより画面位置を取得する。
ターゲット要素のbottom位置が吹き出しのtop位置となる。

吹き出しの左位置は細かな計算をします。
targetRect.left + (targetRect.width / 2) で、ターゲット要素の中央の絶対座標を取得し、
targetRect.left + (targetRect.width / 2) - (overlayRect.width / 2) で、ターゲット要素の中央の絶対座標から、オーバーレイ要素の幅の半分を引きます。これにより、オーバーレイ要素の左端をターゲット要素の中央に合わせることができます。

さらに吹き出しの左位置はclientWidthより右端、もしくは左端に行くときは超えないように調整をしています。

    /**
     * target要素に対するオーバーレイの位置を計算する
     * document.body の範囲内に収まるような値を返す
     *
     * @param {HTMLElement} target - 起点のターゲット要素
     * @param {HTMLElement} overlay - 配置されるオーバーレイ要素
     * @returns {Object} 
     * @returns {number} top - オーバーレイの一番上の位置
     * @returns {number} left - オーバーレイの左位置。
     * @returns {number} targetCenter - ターゲット要素の水平方向の中心。
     */
    function getOverlayPosition(target, overlay) {
        const targetRect = target.getBoundingClientRect();
        const overlayRect = overlay.getBoundingClientRect();
        const bodyWidth = document.body.clientWidth;

        let top = targetRect.bottom;
        let left = targetRect.left + (targetRect.width / 2) - (overlayRect.width / 2);

        // オーバーレイが右端を超えないように調整
        if (left + overlayRect.width > bodyWidth) {
            left = bodyWidth - overlayRect.width;
        }

        // オーバーレイが左端を超えないように調整
        if (left < 0) {
            left = 0;
        }

        return { top, left, targetCenter: targetRect.left + (targetRect.width / 2) };
    }

positionOverlay関数で吹き出しを動的に配置する

positionOverlay関数では、ターゲット要素Idと吹き出し要素Idの文字列を渡して、getOverlayPositionを内部で呼び出し、吹き出しを動的に表示します。

    /**
     * オーバーレイをターゲット要素に対して相対的に配置する。
     *
     * @param {string} targetId - ターゲットId.
     * @param {string} overlayId - オーバーレイId.
     */
    function positionOverlay(targetId, overlayId) {
        const target = document.getElementById(targetId);
        const overlay = document.getElementById(overlayId);
        const arrow = overlay.querySelector('.balloon-arrow');

        if (target && overlay) {
            overlay.style.display = 'block';

            const { top, left, targetCenter } = getOverlayPosition(target, overlay);
            overlay.style.position = 'absolute';
            overlay.style.top = `${top + 16}px`; // add offset
            overlay.style.left = `${left}px`;

            // 矢印の位置を設定
            if (arrow) {
                arrow.style.left = `${targetCenter - left}px`;
            }
        }
    }

最後に初期描画時と、レスポンシブ時に動作するように呼び出します。

    window.addEventListener('DOMContentLoaded', function () {
        positionOverlay('target', 'balloon');
        window.addEventListener('resize', () => positionOverlay('target', 'balloon'));
    });

これでレスポンシブに対応した吹き出しができます。

Summary

DOM位置計算が紙に書きながらやると整理がしやすい。あれ、これどこから左の位置だっけ、どこの計算値だっけ、で迷うことがあった。変数に入れたり、コメントして迷わないようにしたほうが良さそうだ。

よかったらシェアしてね!
目次