【CSS】数秒だけ表示されたのちフェードアウトする吹き出しを超簡単に作る【Copied!】

## 前書き

数秒だけ表示されたのちフェードアウトする吹き出しを簡単に作れる関数を作った。
という話。

別にCSSでもできますが、事前に幅を指定する必要があったりで可変ではないのが嫌だったので汎用的に使える関数を作りました。

## サンプル

https://mogamoga1024.github.io/fukidasi.js/sample.html

## 結論

fukidasi関数を使おう！

```js:fukidasi.js
/**
 * elTargetの吹き出しを表示する
 * 
 * ・（大前提）elTargetはposition:relativeであること！
 * 
 * ・吹き出しの位置はoptionsOrFukidasiPositionにてfukidasiPositionプロパティで指定できる。
 *   値は"top"|"left"|"right"|"bottom"、デフォルトは"bottom"
 *   もしくは"top"|"left"|"right"|"bottom"で直接指定できる。
 * 
 * @param {HTMLElement} elTarget 吹き出しの対象
 * @param {string} text 表示する文字列
 * @param {?{[key: string]: string}|"top"|"left"|"right"|"bottom"} optionsOrFukidasiPosition 吹き出しのCSS設定 or 吹き出しの位置
 */
const fukidasi = (function() {
    let canExecuteMap = new WeakMap();
    const zIndexMax = 2147483647;
    
    /**
     * @param {HTMLElement} elTarget
     * @param {string} text
     * @param {?{[key: string]: string}|"top"|"left"|"right"|"bottom"} optionsOrFukidasiPosition
     */
    return function(elTarget, text, optionsOrFukidasiPosition = null) {
        if (!canExecuteMap.has(elTarget)) {
            canExecuteMap.set(elTarget, true);
        }
        if (!canExecuteMap.get(elTarget)) {
            return;
        }
        canExecuteMap.set(elTarget, false);

        let fukidasiPosition = "bottom";
        let options = null;
        if (typeof optionsOrFukidasiPosition === "string") {
            fukidasiPosition = optionsOrFukidasiPosition;
        }
        else {
            options = optionsOrFukidasiPosition;
        }

        const elFukidasi = document.createElement("div");
        const elTriangle = document.createElement("div");
        Object.assign(elFukidasi.style, {
            padding: "8px",
            textAlign: "center",
            borderRadius: "5px",
            fontSize: "1rem",
            color: "white",
            backgroundColor: "black",
            userSelect: "none",
            zIndex: zIndexMax,
        });

        // カスタムCSSの反映
        Object.assign(elFukidasi.style, options);
        
        elFukidasi.innerText = text;

        elFukidasi.style.position = "fixed";
        elFukidasi.style.visibility = "hidden";
        elTarget.appendChild(elFukidasi);

        const strWidth = getComputedStyle(elFukidasi).width;
        elFukidasi.style.width = strWidth;

        if (options?.hasOwnProperty("fukidasiPosition")) {
            fukidasiPosition = options.fukidasiPosition;
        }
        let triangleBase = 40;
        if (fukidasiPosition === "top" || fukidasiPosition === "bottom") {
            triangleBase = Math.min(triangleBase, elFukidasi.clientWidth / 2);
        }
        else if (fukidasiPosition === "left" || fukidasiPosition === "right") {
            triangleBase = Math.min(triangleBase, elFukidasi.clientHeight / 2);
        }
        const strTriangleBase = `${triangleBase}px`;
        const strTriangleHeight = `${triangleBase / 2}px`;

        if (fukidasiPosition === "top") {
            elFukidasi.style.bottom = `calc(${elTarget.clientHeight}px + ${strTriangleHeight} / 2)`;
            elFukidasi.style.left = `calc((100% - ${elFukidasi.clientWidth}px) / 2)`;
            Object.assign(elTriangle.style, {
                position: "absolute",
                bottom: `calc(-${strTriangleHeight} + 1px)`,
                left: `calc((${elFukidasi.clientWidth}px - ${strTriangleBase}) / 2)`,
                borderLeft: `calc(${strTriangleBase} / 2) solid transparent`,
                borderRight: `calc(${strTriangleBase} / 2) solid transparent`,
                borderTop: `${strTriangleHeight} solid ${elFukidasi.style.backgroundColor}`,
                zIndex: zIndexMax,
            });
        }
        else if (fukidasiPosition === "bottom") {
            elFukidasi.style.top = `calc(${elTarget.clientHeight}px + ${strTriangleHeight} / 2)`;
            elFukidasi.style.left = `calc((100% - ${elFukidasi.clientWidth}px) / 2)`;
            Object.assign(elTriangle.style, {
                position: "absolute",
                top: `calc(-${strTriangleHeight} + 1px)`,
                left: `calc((${elFukidasi.clientWidth}px - ${strTriangleBase}) / 2)`,
                borderLeft: `calc(${strTriangleBase} / 2) solid transparent`,
                borderRight: `calc(${strTriangleBase} / 2) solid transparent`,
                borderBottom: `${strTriangleHeight} solid ${elFukidasi.style.backgroundColor}`,
                zIndex: zIndexMax,
            });
        }
        else if (fukidasiPosition === "left") {
            elFukidasi.style.right = `calc(${elTarget.clientWidth}px + ${strTriangleHeight} / 2)`;
            elFukidasi.style.top = `calc((100% - ${elFukidasi.clientHeight}px) / 2)`;
            Object.assign(elTriangle.style, {
                position: "absolute",
                top: `calc((${elFukidasi.clientHeight}px - ${strTriangleBase}) / 2)`,
                right: `calc(-${strTriangleHeight} + 1px)`,
                borderTop: `calc(${strTriangleBase} / 2) solid transparent`,
                borderBottom: `calc(${strTriangleBase} / 2) solid transparent`,
                borderLeft: `${strTriangleHeight} solid ${elFukidasi.style.backgroundColor}`,
                zIndex: zIndexMax,
            });
        }
        else if (fukidasiPosition === "right") {
            elFukidasi.style.left = `calc(${elTarget.clientWidth}px + ${strTriangleHeight} / 2)`;
            elFukidasi.style.top = `calc((100% - ${elFukidasi.clientHeight}px) / 2)`;
            Object.assign(elTriangle.style, {
                position: "absolute",
                top: `calc((${elFukidasi.clientHeight}px - ${strTriangleBase}) / 2)`,
                left: `calc(-${strTriangleHeight} + 1px)`,
                borderTop: `calc(${strTriangleBase} / 2) solid transparent`,
                borderBottom: `calc(${strTriangleBase} / 2) solid transparent`,
                borderRight: `${strTriangleHeight} solid ${elFukidasi.style.backgroundColor}`,
                zIndex: zIndexMax,
            });
        }

        elTarget.removeChild(elFukidasi);
        elFukidasi.style.position = "absolute";
        elFukidasi.style.visibility = "";

        const fadeOutDuration = 2000;
        elFukidasi.animate([
            {
                visibility: "visible",
                opacity: 1
            },
            {
                visibility: "hidden",
                opacity: 0
            }
        ], {
            duration: fadeOutDuration,
            easing: "ease-in",
            fill: "forwards"
        });

        elTarget.appendChild(elFukidasi);
        elFukidasi.appendChild(elTriangle);

        setTimeout(() => {
            elTarget.removeChild(elFukidasi);
            canExecuteMap.set(elTarget, true);
        }, fadeOutDuration);
    }
})();
```

## 使い方

とりあえず吹き出しを出したい要素を`position: relative;`にして
`onclick="fukidasi(this, 'メッセージ')"`とすれば吹き出しがでます。

もっと詳しいことは以下を読んで察してください。（説明放棄）

```sample.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>吹き出しサンプル</title>
  <link rel="stylesheet" href="sample.css">
</head>
<body>
  <div class="btn-container">
    <button class="btn" onclick="fukidasi(this, '左！！', 'left')">押してね</button>
    <button id="hoge" class="btn">押してね</button>
    <button id="fuga" class="btn">押してね</button>
    <button id="piyo" class="btn">押してね</button>
  </div>

  <script src="fukidasi.js"></script>
  <script>
    document.querySelector("#hoge").onclick = function() {
        const options = {
            fontSize: "1.2rem",
            fontWeight: "bold",
            color: "black",
            backgroundColor: "yellow",
            fukidasiPosition: "top"
        };
        fukidasi(this, "こんにちは", options);
    };
    document.querySelector("#fuga").onclick = function() {
        const options = {
            padding: "20px",
            fontSize: "1.5rem",
            fontWeight: "bold",
            color: "red",
            backgroundColor: "green"
        };
        fukidasi(this, "こんばんわ", options);
    };
    document.querySelector("#piyo").onclick = function() {
        const options = {
            padding: "8px",
            fontSize: "1.3rem",
            fontWeight: "bold",
            color: "black",
            backgroundColor: "pink",
            fukidasiPosition: "right"
        };
        fukidasi(this, "Hello!", options);
    };
  </script>
</body>
</html>
```

```sample.css
:root {
  background-color: aquamarine;
}

.btn-container {
  display: flex;
  margin: 120px;
  flex-wrap: wrap;
  column-gap: 16px;
  row-gap: 32px;
}

.btn {
  position: relative;
  width: 120px;
  height: 50px;
  font-size: 1.2rem;
}
```

以上

https://github.com/mogamoga1024/fukidasi.js

## 似ている記事

https://qiita.com/7note/items/bfe36b8fce2206eb824f