見出し画像

【JavaScript】セパレータごとにページ内リンクを自動生成する方法

うっかり5万字の長文を書いてしまった際に、

改ページはしたくない!でも読み手には不親切すぎる!

……ということでこの方法を思いつきました。
でもいちいち手打ちでid付与してリンク作って……というのも面倒臭かったので、すべて動的に生成してしまえ、という横着さ加減によるものです。
HTML + CSS + JavaScriptだけで実装しています。


デモサンプル

1. 前提

前提として、自分の書く小説にはセクション分けにの際に以下のようなセパレータを使用しています。

  <p class="sep">*   *   *</p>

このセパレータを区切りとして1セクションとして区切り、pixivに掲載する際にはそれを基準として改ページを行っています。
そのため、このセパレータに対しidを付与し、それに対しページ内リンクを生成する、というものです。
条件としては以下のような感じになります。

  1. .sep ごとに #section-n というidを付与する。

  2. ただし、一番最初のセクションにはセパレータが存在しないため、別途 #top へのリンクを生成する。また、このときリンクテキストは 1 とする。

  3. そのため、その次に存在する最初の .sep のidは #section-2 、リンクテキストは 2 になる。

  4. 以下、+1ずつ増えていく。(コード的には #top を起点とするため、+2)

  5. URLの #section-n と一致するページ内リンクに対して .active を付与する。

  6. ページ内リンクのブロックは .sec とする。


2. デモ


3. コード

HTML

HTMLは以下のような感じです。

<div class="wrap">
  <p>
    <!-- 本文 1 -->
  </p>
  <p class="sep">*   *   *</p>
  <p>
    <!-- 本文 2 -->
  </p>
  <p class="sep">*   *   *</p>
  <p>
    <!-- 本文 3 -->
  </p>
  <p class="sep">*   *   *</p>
  <p>
    <!-- 本文 4 -->
  </p>
</div>

本文のパラグラフにはclassもidもありません。
セパレータにはセンタリングと余白を指定するCSSのみが設定ししてありました。
これに対し、先程の条件どおりにidを付与すると以下のような感じになります。

<dody id="top">
  <div class="wrap">
    <p>
      <!-- 本文 1 -->
    </p>
    <p class="sep" id="section-2">*   *   *</p>
    <p>
      <!-- 本文 2 -->
    </p>
    <p class="sep" id="section-3">*   *   *</p>
    <p>
      <!-- 本文 3 -->
    </p>
    <p class="sep" id="section-4">*   *   *</p>
    <p>
      <!-- 本文 4 -->
    </p>
  </div>
</body>

これに対し生成されるリンクは以下のようなものです。

<div class="sec">
  <a href="#top">1</a>
  <a href="#section-2">2</a>
  <a href="#section-3">3</a>
  <a href="#section-4">4</a>
</div>

JavaScript

この程度にjQueryとか冗談ではないしjQueryが嫌いな人間なので、Vanilla JSでやります。

window.onload = function() {
  var secLines = document.querySelectorAll('.sep');

  if (!secLines.length) return;

  var sec = document.createElement('div');
  sec.classList.add('sec');

  var topLink = document.createElement('a');
  topLink.href = '#top';
  topLink.innerHTML = '1';
  sec.appendChild(topLink);

  topLink.addEventListener('click', function() {
    var currentActive = document.querySelector('.sec a.active');
    if (currentActive) currentActive.classList.remove('active');
    this.classList.add('active');
  });

  for (var i = 0; i < secLines.length; i++) {
    secLines[i].id = 'section-' + (i + 2);
    var link = document.createElement('a');
    link.href = '#section-' + (i + 2);
    link.innerHTML = (i + 2);
    sec.appendChild(link);

    link.addEventListener('click', function() {
      var currentActive = document.querySelector('.sec a.active');
      if (currentActive) currentActive.classList.remove('active');
      this.classList.add('active');
    });
  }

  document.body.appendChild(sec);

  // .activeを付与
  var hash = window.location.hash;
  if (hash) {
    var targetLink = document.querySelector('.sec a[href="' + hash + '"]');
    if (targetLink) {
      targetLink.classList.add('active');
    }
  }
};

前提条件にも書いてあるように、所謂1ページ目に相当するのはページの一番最初のセクションですが、これにはセパレータが存在しないため、#top へのリンクをリンクテキスト 1 として生成します。
更にリンクをクリックした際に .active を与える処理も別になります。

  var topLink = document.createElement('a');
  topLink.href = '#top';
  topLink.innerHTML = '1';
  sec.appendChild(topLink);

  topLink.addEventListener('click', function() {
    var currentActive = document.querySelector('.sec a.active');
    if (currentActive) currentActive.classList.remove('active');
    this.classList.add('active');
  });

あとは最初のセパレータを2ページ目相当として、2ずつカウントアップしていきます。

  for (var i = 0; i < secLines.length; i++) {
    secLines[i].id = 'section-' + (i + 2);
    var link = document.createElement('a');
    link.href = '#section-' + (i + 2);
    link.innerHTML = (i + 2);
    sec.appendChild(link);

    link.addEventListener('click', function() {
      var currentActive = document.querySelector('.sec a.active');
      if (currentActive) currentActive.classList.remove('active');
      this.classList.add('active');
    });
  }

CSS

.sec {
  width:100%;
  background:#fff;
  border-top:1px solid #dcdddd;
  position:fixed;
  bottom:0;
  left:0;
  text-align:center;
  padding:.5em;
  transition: .5s;
}
.sec a {
  display:inline-block;
  color:#555;
  background:#e7e7eb;
  border-radius:50%;
  height:40px;
  width:40px;
  line-height:40px;
  text-decoration:none;
}
.sec a:hover { background:#fdeff2; }
.sec a + a { margin-left:.5em; }
.sec .active { background:#e4d2d8; }

a要素しか存在しないので、リンクとリンクの間には余白を設けてあります。
また、スマートフォンでもクリックしやすいように、各リンクには inline-block にして幅と高さを設定してあります。
リスト要素で括っているわけではないので、余計な隙間は存在しないため、今回は考慮していません。


4. スクロールしたら隠す

この状態でもほぼ完成です。
ですが、小説を読んでいるときにリンクが常に見えている状態というのも目障りなものです。
よって、よくある「スクロールしたら要素を隠し、ちょっと上にスクロールすると表示」というよくあるUIを追加で入れてみました。

// スクロールしたら隠す
var startPos = 0, winScrollTop = 0;

window.addEventListener('scroll', function() {
  winScrollTop = window.pageYOffset;
  if (winScrollTop >= startPos) {
    if (winScrollTop >= 200) {
      document.querySelector('.sec').classList.add('hide');
    }
  } else {
    document.querySelector('.sec').classList.remove('hide');
  }
  startPos = winScrollTop;
});

CSSはたった一行。
.hide でページ内リンク用の要素を画面外に持っていきます。

.sec.hide { transform: translateY(120%); }

以上でした。

実のところ、これを実装したページ自体はWordPressで作っています。
しかし、改ページのためのリンクを追加するのが面倒だったのと、将来的にアーカイブ化する際に静的HTMLにすることを前提としていたので、確実な方法を取りました。
ちなみに、前述のようにWordPressのテンプレートに組み込んだので、セパレータの存在しないページではリンクは生成されません。

この記事が気に入ったらサポートをしてみませんか?