気まま研究所ブログ

ITとバイク、思ったことをてきとーに書きます。

jQuery マウスオーバーで画像をマウスの横に出す

とあるサイトでリンクをマウスオーバーすると画像が横に出るスクリプトが入っていて、欲しくなったので実装してみました。
元々個人で運用しているブログで使う専用に書いたのですが、更新ネタもないのでこっちにも残しておきます。
これと同時にクリックやマウスオーバーで画像を切り替えるスクリプトも掲載するのでよしなに使ってください。

なお、JavaScript/jQueryは専門外なので流行りや効率が必要ならいい感じに書き換えてください。

検証環境

項目 詳細
Google Chrome 80.0.3987.132, 64bit
Safari 13.0.5 (15608.5.11)
JQuery v3.4.1

これ以外の環境は検証はしてないし、各関数、メソッドが対応しているかどうかすらチェックしてません。
古いブラウザとか対応する予定なら細かいところのチェックが必要です。

やること

オーバーレイ用に隠し要素を置いておき、特定のリンクをマウスオーバー(マウスムーブだけど)するとそれが表示されるようにします。
表示させると同時にオーバーレイ内のimg要素に画像URLを設定することで特定の画像を表示させます。
また、マウスオーバーの際にマウス座標からオーバーレイの座標を計算し、マウスからちょっと右側に表示させることで目線に来るようにしています。

カーソル写らなかったから手書きだけどこんな感じ

マウスを動かせばリンクをマウスオーバーしてる限り追随してくれます。

なお、画像サイズによってははみ出るので、反対側に表示させたり、それでもはみ出る場合は強制的に画面内に留まるように調整しました。
position fixedで他に要素置いてたりするとかぶるのでその辺りは要調整です。

サンプル

今回のサンプルはこちらです。
HoverableLink

functionで書いたSafariでも動くやつ
HoverableLinkSafari

実装

HTML

まずはHTMLから。

オーバーレイ用の要素を一番上に置きます。
CSSでも述べますが、どうせfixedにするのでどこにおいても大丈夫です。
通常はいらないので .invisible で非表示にしておきます。
なお、classの名前はJSの初期化で指定するのでなんでもいいです。

<div class="image-overlay invisible">
    <img />
</div>

リンクはdata-srcに表示させたい画像のリンクを書いておきます。
class名は同様にJSで指定するのでhoverable-linkでなくてもOK。

<a href="javascript:void(0)" class="hoverable-link" data-src="20200215_081237419_iOS.jpg">test</a><br />

で、お次がJavaScriptの初期化です。

$(function () {
    let hoverableLink = new HoverableLink();
    hoverableLink.register(".hoverable-link", ".image-overlay");
    hoverableLink.preload();
});

HoverableLinkは最後に出しますが、HoverableLink#registerでリンクのclassとオーバーレイのclassを指定します。
すると当該classを持つリンクは全てマウスオーバーでオーバーレイが出るようになります。

以下HTML全文

<html>
<head>
    <meta charset="utf-8" />
    <title>HoverableLink</title>
    <link rel="stylesheet" type="text/css" href="image.css" />

    <!-- サンプル用の位置調整だから不要 -->
    <style type="text/css">
    .top-left {
        position: absolute;
    }
    .top-right {
        position: absolute;
        right: 410px;
    }
    .center-left {
        position: absolute;
        top: 50%;
    }
    .bottom-left {
        position: absolute;
        bottom: 0;
    }
    </style>

</head>

<body>
    <div class="image-overlay invisible">
        <img />
    </div>

    <a href="javascript:void(0)" class="hoverable-link top-left" data-src="20200215_081237419_iOS.jpg">test</a><br />
    <a href="javascript:void(0)" class="hoverable-link top-right" data-src="20200215_081252249_iOS.jpg">test2</a><br />
    <a href="javascript:void(0)" class="hoverable-link center-left" data-src="20200215_081237419_iOS.jpg">test</a><br />
    <a href="javascript:void(0)" class="hoverable-link bottom-left" data-src="20200215_081237419_iOS.jpg">test</a><br />

    
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="HoverableLink.js"></script>
    <script type="text/javascript">
        $(function () {
            let hoverableLink = new HoverableLink();
            hoverableLink.register(".hoverable-link", ".image-overlay");
            hoverableLink.preload();
        });
    </script>
</body>
</html>

CSS

特に注記すべきところは無し。
ぶっちゃけオーバーレイを好きな場所に置ける状態ならなんでもOK
今回は.image-overlayをposition fixedにしているので好きなところに配置できます。
また、.visibleのdisplay blockは定義被りで無効化される恐れがあるので!importantにしておきます。

div.image-overlay {
    position: fixed;
    z-index: 999;
    border: 1px solid;
    max-width: 50%;
}

div.invisible {
    display: none;
}

div.visible {
    display: block !important;
}

div.image-overlay img {
    width: 100%;
}

JavaScript

本題がこちら。
まず、HoverableLink#registerにてリンクのクラスとオーバーレイのクラスを登録します。
次に、リンクのクラスに対してmousemoveとmouseleaveイベントを登録し、オーバーレイはフィールドに格納しておきます。

mousemoveイベントの本体であるhoverImage関数(この場合JSでもメソッドというのか?)ではオーバーレイ内のimg要素のsrc属性にURLを設定し、座標を計算した後にvisibleクラス(cssで表示状態を定義)を追加し、オーバーレイを表示させます。
なお、画像URLはリンクのdata-src属性から引っ張っています。

座標計算はcalcPoint関数で行っていますが、オーバーレイのサイズとウィンドウのサイズからはみ出ないように反対側に表示させたり、反対側に表示させた場合さらにはみ出る場合があるので強制的に画面内に留まるようにしています。
ただし、画像の読み込みが遅いと座標計算がズレたり、極端な画面比の場合はうまく動かないかもしれないです。

mouseleaveイベントで実行されるleaveImage関数ではオーバーレイの非表示設定とimg要素のsrc属性を空にする処理をしています。
空にしないとたまにマウスオーバーした瞬間に前の画像が一瞬写ることがあります。

また、画像の容量がでかい場合は表示に時間がかかるので、preload関数を呼び出せば予めプリロードできます。

百聞は一見に如かず、ソースコードを眺めるほうが説明を見るよりわかると思います。

class HoverableLink {

    className = '';
    overlayClassName = '';
    overlayClassElem = '';
    imgElem = '';
    exceptedWidth = 0;
    exceptedHeight = 0;

    register = (classNameArg, overlay) => {
        let self = this;
        this.overlayClassName = overlay;
        this.className = classNameArg;
        let elem = $(classNameArg);

        this.overlayClassElem = $(this.overlayClassName);
        this.imgElem = $(this.overlayClassElem.children("img"));

        elem.each(function (index, element) {
            element.addEventListener("mouseenter", function(event) {
                self._showImage(event, $(element).data('src'));
            });
            element.addEventListener("mousemove", function (event) {
                self._hoverImage(event, $(element).data('src'));
            }, false);
            element.addEventListener("mouseleave", self._leaveImage, false);
        });
    };

    preload = async () => {
        let elem = $(this.className);
        elem.each(function (index, element) {
            let src = $(element).data('src');
            let image = new Image();
            image.src = src;
        });
    };

    _showImage = (event, src) => {
        let self = this;
        this.imgElem.attr("src", src);
        this.overlayClassElem.css({ top: String(0) + 'px', left: String(0) + 'px' });

        this.imgElem.on("load", function() {
            self.exceptedWidth = self.overlayClassElem.width();
            self.exceptedHeight = self.overlayClassElem.height();

            let [pointX, pointY] = self._calcPoint(event, self.overlayClassElem);
            self.overlayClassElem.css({ top: String(pointY) + 'px', left: String(pointX) + 'px' });

            if (!self.overlayClassElem.hasClass("visible"))
                self.overlayClassElem.addClass("visible");

            self.imgElem.off("load");
        });
    };

    _hoverImage = (event, src) => {
        if (this.exceptedWidth <= 0 || this.exceptedHeight <= 0)
            return;

        let [pointX, pointY] = this._calcPoint(event, this.overlayClassElem);
        this.overlayClassElem.css({ top: String(pointY) + 'px', left: String(pointX) + 'px' });
    };

    _calcPoint = (event, elem) => {
        let windowWidth = window.innerWidth;
        let windowHeight = window.innerHeight;
        let exceptedWidth = elem.width();
        let exceptedHeight = elem.height();
        let pointX = event.clientX;
        let pointY = event.clientY;

        // オーバーレイがウィンドウ右側からはみ出るかどうか
        // はみ出たら逆になるように計算し、さらにはみ出る場合は強制的に0にする
        if (windowWidth - (event.clientX + exceptedWidth + 25) > 0) {
            pointX += 25;
            pointX = pointX + exceptedWidth > windowWidth ? windowWidth - exceptedWidth : pointX;
        }
        else {
            pointX -= 25 + exceptedWidth;
            pointX = pointX < 0 ? 0 : pointX;
        }

        // オーバーレイがウィンドウ下側からはみ出るかどうか
        // はみ出たら逆になるように計算し、さらにはみ出る場合は強制的に0にする
        if (windowHeight - (event.clientY + exceptedHeight) > 0) {
            pointY += 25;
        }
        else {
            pointY -= 25 + exceptedHeight;
            pointY = pointY < 0 ? 0 : pointY;
        }
        return [pointX, pointY];
    };

    _leaveImage = (event) => {
        if (this.overlayClassElem.hasClass("visible"))
            this.overlayClassElem.removeClass("visible");
        let src = this.imgElem.attr("src");
        if (src !== "")
            this.imgElem.attr("src", "");

        this.exceptedWidth = 0;
        this.exceptedHeight = 0;
    };
}

JavaScript (Safari)

後からclass構文にしたので動くもんだと思ってましたが、今確認したらSafari全く動かなかったのでfunction版に戻したやつも置いておきます。

function HoverableLink() {

    this.register = (classNameArg, overlay) => {
        overlayClassName = overlay;
        className = classNameArg;
        let elem = $(classNameArg);
        let self = this;

        overlayClassElem = $(overlayClassName);
        imgElem = $(overlayClassElem.children("img"));

        elem.each(function (index, element) {
            element.addEventListener("mouseenter", function(event) {
                showImage(event, $(element).data('src'));
            });
            element.addEventListener("mousemove", function (event) {
                hoverImage(event, $(element).data('src'));
            }, false);
            element.addEventListener("mouseleave", leaveImage, false);
        });
    }

    this.preload = async () => {
        let elem = $(className);
        elem.each(function (index, element) {
            let src = $(element).data('src');
            let image = new Image();
            image.src = src;
        });
    }

    let className = '';
    let overlayClassName = '';
    let overlayClassElem = '';
    let imgElem = '';
    let exceptedWidth = 0;
    let exceptedHeight = 0;

    let showImage = (event, src) => {
        imgElem.attr("src", src);
        overlayClassElem.css({ top: String(0) + 'px', left: String(0) + 'px' });

        imgElem.on("load", function() {
            exceptedWidth = overlayClassElem.width();
            exceptedHeight = overlayClassElem.height();

            let [pointX, pointY] = calcPoint(event, overlayClassElem);
            overlayClassElem.css({ top: String(pointY) + 'px', left: String(pointX) + 'px' });

            if (!overlayClassElem.hasClass("visible"))
                overlayClassElem.addClass("visible");
        });
        
    }

    let hoverImage = (event, src) => {
        if (exceptedWidth <= 0 || exceptedHeight <= 0)
            return;

        let [pointX, pointY] = calcPoint(event, overlayClassElem);
        overlayClassElem.css({ top: String(pointY) + 'px', left: String(pointX) + 'px' });
    }

    let calcPoint = (event, elem) => {
        let windowWidth = window.innerWidth;
        let windowHeight = window.innerHeight;
        let pointX = event.clientX;
        let pointY = event.clientY;

        if (windowWidth - (event.clientX + exceptedWidth + 25) > 0) {
            pointX += 25;
            pointX = pointX + exceptedWidth > windowWidth ? windowWidth - exceptedWidth : pointX;
        }
        else {
            pointX -= 25 + exceptedWidth;
            pointX = pointX < 0 ? 0 : pointX;
        }
        
        if (windowHeight - (event.clientY + exceptedHeight) > 0) {
            pointY += 25;
        }
        else {
            pointY -= 25 + exceptedHeight;
            pointY = pointY < 0 ? 0 : pointY;
        }
        return [pointX, pointY];
    }

    let leaveImage = (event) => {
        if (overlayClassElem.hasClass("visible"))
            overlayClassElem.removeClass("visible");
        let src = imgElem.attr("src");
        if (src !== "")
            imgElem.attr("src", "");

        exceptedWidth = 0;
        exceptedHeight = 0;

        imgElem.off("load");
    }
}