🖇️

外部リンクをクリックしたときだけ別タブを開く方法について考える

2024/06/10に公開

概要

HTMLで別ウィンドウを開くリンクを張る場合、<a target='_blank'>のように、<a>要素にtarget属性を設定する必要があります。

よくあるパターンとして、内部リンクは同じタブ内で、外部リンクは別タブ(ウィンドウ)で開きたいという状況があります。
ですが内部リンクか外部リンクかを判断しながらHTMLを書いていくのはどうにも面倒くさい。
そんなずぼらな人向けのJavaScriptを考えてみました。

経緯

考えたきっかけはGitea(Gitのリモートサーバー)をセルフホストしたこと。
Wikiに記載したリンクの「内部リンクは同じタブ」で、「外部リンクは別タブ」で開きたいと思ったので考えてみました。

サンプルコード

HTMLファイルの先頭あたりに追加しましょう。

サンプルコード
document.addEventListener('click', event => {
    if (event.target.tagName.toUpperCase() === 'A' && event.target['target'] === '') {
        let href = event.target['href'];
        if (href !== '' && new URL(href, window.location.href).host !== new URL(window.location.href).host) { // ※
            window.open(href, '_blank');
            event.stopPropagation();
            event.preventDefault();
        }
    }
});

前提として、今回のサンプルコードは「異なるドメイン」の場合を外部リンクとみなして別ウィンドウを開くコードです。
つまりhttps://xxxx.yyyy.jp:8080/AAAA/bbbb/ccc.htmlxxxx.yyyy.jp:8080が一致しているかどうかで判断しています。

サブディレクトリまで含んだURL(xxxx.yyyy.jp:8080/AAAA)までを内部リンクとする場合は、サンプルコードの※印部分を改造する必要があることをご注意ください。(改善案の項にて解説しています)

解説

new URLURLとは

今回のサンプルコードでは、URL APIを使用しています。

https://developer.mozilla.org/ja/docs/Web/API/URL_API

これはURL文字列を雑に引数として放り込むとURLとして分析してくれるという便利なAPIで、今回はドメインを比較する工程で使用しています。

  • 比較左辺:new URL(href, window.location.href).host
  • 比較右辺:new URL(window.location.href).host

比較式左辺では「<a>要素がリンク先としているURL」を分析しています。
第1引数がリンク先、第2引数が現在のURLを指定することで、href要素が相対参照(./page.html)やハッシュ(#~~)であった場合に現在のURLが基準であることを示しています。
ここで第1引数が絶対参照(https:////で始まるURL)であった場合、URL()の第2引数は無視される仕様になっています。

比較式右辺は「現在のURL」を分析しています。
window.location.hrefは絶対参照のURLを返すので、第2引数の指定はしません。

https://developer.mozilla.org/ja/docs/Web/API/URL/URL#例

また、.hostプロパティはURL APIで解析したURLのドメインとポート番号を返します。
つまりhttps://xxxx.yyyy.jp:8080/AAAA/bbbb/ccc.htmlxxxx.yyyy.jp:8080が取得できるものです。

https://developer.mozilla.org/ja/docs/Web/API/URL/host

私が今回の課題に直面したGiteaでは、「https://xxxx.yyyy.jp:8080/AAAA/bbbb/ccc.htmlhttps://xxxx.yyyy.jp:8080/AAAAをルートとする!」というようにサブディレクトリの指定が原則できません。(推奨されておらず、リバースプロキシなどを設定するしかありません)
そのため今回以上の実装は必要がありませんでした。
通常レンタルブログなどは、サブドメイン(https://xxxx.yyyy.jp:8080/でいうxxxxの部分)が変わることが多いので大抵の場合はこれで問題ないと思います。

改善案、サブディレクトリにも対応するためには

ですが、それでも今回のコードをサブディレクトリに対応したい場合はどうすればいいか。

試していないので確実なことは言えませんが、まずサブディレクトリを含んだURLを取得する必要があると思います。
これには「JavaScriptで<a>要素のhref属性にURLを代入すると絶対参照のURLが設定される」という仕様を使ってみるのがよいでしょう。
https://qiita.com/sounisi5011/items/e24d0e1dfd71c5dd528e

サブディレクトリを含んだURLのルートパスを取得する
let elmn = document.createElement('a');
elmn.href = '/'; // ←ルートとなるルート相対パス(つまり「/」)を代入
console.info(elmn.href); // ←elmn.hrefにはルートとなる絶対参照のURLが設定されている

あとは比較右辺new URL(window.location.href).hostをこのサンプルコードから得られるURLに変更して、左辺はスラッシュの数を数えて切り抜くなり、文字列操作のstartsWith()を使うなりして比較すればうまくいくのではないかなと思います。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith

試していないのでサンプルコードは掲載しませんが、おおよそうまくいく気がします。

おまけ

なんでdocument.addEventListener('click')

HTMLがJavaScriptによって動的に変わるため、<a>要素に直接イベントを実装しても、あとから追加された<a>要素にイベントが反映されないため。
こういった場合は「ドキュメントそのものにイベントを張り、event.target(クリックされた要素)がなんなのか」を判定します。

なんでtoUpperCase()

精神衛生上、文字列の比較は「大文字か小文字に明示的に変換」してやりたかったので……
JavaScriptの仕様上、明らかに不要なコードではあるので消して使用するのがよいでしょう

Discussion