Ajaxでページングした一覧ページから詳細ページへ遷移後にブラウザバックで一覧に戻っても、GETで一覧に戻っても、POSTで一覧に戻っても、F5でリロードしてもクリックした明細が画面内に表示されるサンプル

## 概要

次の一連の動作後の詳細ページから、ブラウザバックをした時に、スクロールで表示させた目的の明細が表示領域にある状態にしたい。

1. データ一覧をAjaxでページングする
1. 目的のデータまでスクロールする
1. 目的のデータのリンクをクリックして詳細ページを開く


## 目的

次の動作を行った時、データ一覧ページにて、クリックしたデータ行が、表示領域にある事

* 明細ページからブラウザバックした時
* 明細ページからGETやPOSTのサブミットで一覧ページに戻った時
* 一覧ページでF5でリロードした時
* 一覧ページでCtrl+F5でキャッシュ削除リロードした時
* 一覧ページでブラウザバックした時に前に表示していたページを表示とともに、前回選択したデータ行にフォーカス
* 複数のタブで開いても、独立して処理される事。別のタブに影響を与えない事。サーバーのセッションやローカルストレージを使用しない事。（追記：セッションストレージは使える）

## サンプルのURL

[Ajaxでページングして正しく表示領域の更新と履歴の更新をする](https://yamine1san.ddns.net/qiita/ajax-paging/index.php)

## サンプルプログラム

```index.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Ajaxでページングして正しく表示領域の更新と履歴の更新をする</title>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script>

      /**
       * Get the URL parameter value
       *
       * @see https://www-creators.com/archives/4463
       * @param  name {string} パラメータのキー文字列
       * @return  url {url} 対象のURL文字列（任意）
       */
      function getParam(name, url) {
          if (! url) url = window.location.href;
          name = name.replace(/[\[\]]/g, "\\$&");
          var regex = new RegExp("[?&]"+name+"(=([^&#]*)|&|#|$)"),
              results = regex.exec(url);
          if (! results) return null;
          if (! results[2]) return '';
          return decodeURIComponent(results[2].replace(/\+/g, " "));
      }

      window.addEventListener('pageshow', function (e) {
          const row_no = getParam('row_no');
          if (row_no) {
              forcusAndHighlightRow(row_no);
          }
          else {
              forcusAndHighlightRow($('input[name="row_no"]').val());
          }
      });

      window.addEventListener('popstate', function (e) {
          if (window.history && window.history.pushState) {
              const state = e.state;
              if (state) {
                  openPage(state);
              }
              else {
                  openPage({'page_no': 0, 'row_no': 0});
              }
          }
      });

      window.addEventListener('DOMContentLoaded', function () {

          // 詳細ページクリックへのバインド
          bindLinkDetail();

          // ページング番号クリックへのバインド
          $('.change-page').unbind('click').bind('click', function () {
              const page_no = $(this).attr('page_no');
              const state = {
                  'page_no': page_no,
                  'row_no': 0
              };
              openPage(state);
              if (window.history && window.history.pushState) {
                  const url = 'index.php?page_no='+state.page_no;
                  window.history.pushState(state, '', url);
              }
          });
      });

      /**
       *
       * @param state
       */
      function openPage(state) {
          if (state.page_no < 1) {
              return;
          }
          $.get("table.php", {'page_no': state.page_no}, function (html) {
              html = '<div>Ajaxで取得したコンテンツ</div>'+html;
              $('#page-list').html(html);
              // 詳細ページクリックへのバインド
              bindLinkDetail();
              forcusAndHighlightRow(state.row_no);
          });
      }

      /**
       * 詳細ページクリックへのバインド
       */
      function bindLinkDetail() {
          $('.a-detail').unbind('click').bind('click', function () {

              $('#page-list').find('tr').removeClass('current');
              $(this).closest('tr').addClass('current');

              const page_no = $(this).attr('page_no');
              $('input[name="page_no"]').val(page_no);

              const row_no = $(this).attr('row_no');
              $('input[name="row_no"]').val(row_no);

              if (window.history && window.history.pushState) {
                  const state = {
                      'page_no': page_no,
                      'row_no': row_no
                  };
                  const url = 'index.php?page_no='+page_no+'&row_no='+row_no;
                  window.history.pushState(state, '', url);
              }
              $(this).closest('form').submit();
              return false;
          });
      }

      /**
       *
       * @param row_no
       */
      function forcusAndHighlightRow(row_no) {
          if (row_no < 1) {
              $('#page-list').scrollTop(0);
              return;
          }
          $('.page-list').find('tr').removeClass('current');
          const row = $('#row'+row_no);
          row.addClass('current');
          const div = $('#page-list');
          div.scrollTop(row.position().top-(div.height() / 2)+row.height());
      }
  </script>
  <style>
   .current td {
    border: 3px solid blue;
   }
  </style>
</head>
<body>
<form method="POST" action="detail.php">
  <input type="hidden" name="page_no" value="<?php echo (array_key_exists('page_no', $_REQUEST)) ? $_REQUEST['page_no'] : ''; ?>"/>
  <input type="hidden" name="row_no" value="<?php echo (array_key_exists('row_no', $_REQUEST)) ? $_REQUEST['row_no'] : ''; ?>"/>
  <a href="javascript:void(0);" class="change-page" page_no="1">1ページ目</a>
  <a href="javascript:void(0);" class="change-page" page_no="2">2ページ目</a>
  <a href="javascript:void(0);" class="change-page" page_no="3">3ページ目</a>
  <a href="javascript:void(0);" class="change-page" page_no="4">4ページ目</a>
  <a href="javascript:void(0);" class="change-page" page_no="5">5ページ目</a>

  <div id="page-list" style="height:500px;overflow:auto;border:1px solid #000;">
    <div>初期表示のコンテンツ</div>
      <?php
      // F5押下時の初期表示処理
      if (array_key_exists('page_no', $_REQUEST) && 0 < $_REQUEST['page_no']) {
          require './table.php';
      }
      ?>
  </div>
</form>
</body>
```

```table.php
<table>
    <?php for ($i = 1; $i <= 100; $i++): ?>
      <tr id="row<?php echo $i; ?>">
        <td>
          <a class="a-detail"
             page_no="<?php echo $_REQUEST['page_no']; ?>"
             row_no="<?php echo $i; ?>"
             href="detail.php?page_no=<?php echo $_REQUEST['page_no']; ?>&row_no=<?php echo $i; ?>">
              <?php echo $_REQUEST['page_no']; ?>ページ目の<?php echo $i; ?>番の詳細
          </a>
        </td>
      </tr>
    <?php endfor; ?>
</table>
```

```detail.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>明細ページ</title>
</head>
<body>

このページは<?php echo $_REQUEST['page_no']; ?>ページ目の<?php echo $_REQUEST['row_no']; ?>の詳細ページです。
<br>
ブラウザバックで戻った時の動作とフォームのPOSTで戻った時の動作確認用

<fieldset>
  <legend>method="GET"</legend>
  <form method="GET" action="index.php">
    <input type="hidden" name="page_no" value="<?php echo $_REQUEST['page_no']; ?>"/>
    <input type="hidden" name="row_no" value="<?php echo $_REQUEST['row_no']; ?>"/>
    <input type="submit" value="GETサブミット"/>
  </form>
</fieldset>

<fieldset>
  <legend>method="POST"</legend>
  <form method="POST" action="index.php">
    <input type="hidden" name="page_no" value="<?php echo $_REQUEST['page_no']; ?>"/>
    <input type="hidden" name="row_no" value="<?php echo $_REQUEST['row_no']; ?>"/>
    <input type="submit" value="POSTサブミット"/>
  </form>
</fieldset>

</body>
</html>
```

## ハマりどころ

* ブラウザバックでFirefoxはキャッシュ表示、Chromeはリクエストするなど、数多
* 画面遷移のデータ保持に、サーバーのセッションを使ったり、ローカルストレージ使用すると、複数タブで作業した時に片方がもう片方に影響を与えるので、リクエストパラメータだけで処理した
（追記：コメント頂いた通りセッションストレージは問題なし）