
白雨日記の記事リストを無限スクロールにしました。
サイト
Lagon Journal
なにをどうしたか
やったことはだいたい以下のような感じです。ひとつずつご紹介していきます。
- 最大表示数を50件に決める
- 最初は10件だけ表示して、11件目以降はCSSで隠す
- スクロールに合わせて10件ずつ追加で表示する
- 記事リストにブラウザバックしたときに、さっき表示していたところまでは表示する
1. 最大表示数を50件に決める
{{ $paginator := .Paginate $pages.ByDate.Reverse 50 }}
$pagesは、すべての日記をあらわしています。「$pages.ByDate.Reverse 50」で最新の50件を取ってきています。
2. 最初は10件だけ表示して、11件目以降はCSSで隠す
{{ range $index, $el := $paginator.Pages }}
<article id="shirasame-article{{ $index }}" class="header-list {{ if gt $index 9 }}none{{end}}">
<!-- (省略) -->
</article>
{{ end }}
.none {
display: none;
}
HTMLタグの中でも変数や関数が使えるというHUGOの便利機能を活用します。「gt A B」は「A > B」かどうかを判定しています。HUGOの独特な書き方です。articleタグのidに0から始まる連番を振って、もしその連番が9よりも大きかったら、要素の存在を隠すnoneというclassを付けるようにしました。これで、最初は10件だけが表示されて、残りの40件は隠されるようになりました。
3. スクロールに合わせて10件ずつ追加で表示する
ここからJavaScript(JS)を使っていきます。全体がそれなりに長いので、小分けして紹介していきます。手っ取り早く全体図だけ見たいかたは、いちばん下に全体図を載せてあるので、ガツンと下までスクロールしてください。
スクロールイベントをつくる
let timeout_id = 0; //処理を実行するかどうかのフラグ
//-----------------
window.addEventListener("scroll", SetDelay);
function SetDelay() {
if (timeout_id) return;
timeout_id = setTimeout(function () {
WatchScrolling();
timeout_id = 0;
}, 100);
}
スクロールイベントにSetDelay関数を結びつけています。SetDelay関数は、スクロールイベントを100ミリ秒ごとに間隔をあけて実行させるようにする関数です。実はなくてもそんなに問題ないのかもしれませんが、スクロールイベントはたくさん起こるため、ちょっとスクロールするとすぐ100回とかいったりするのがちょっといやだったのでつけました。でも正直100ミリ秒くらいならあってもなくてもあんまり変わらないような気もしています。
SetDelay関数の中でWatchScrolling関数を呼び出しています。こちらが無限スクロールのおもな流れを牛耳っている関数です。もうちょっといい名前あったんじゃないかと思ったり思わなかったりしています。
スクロールしたときにやることをまとめたもの
const init_num = 9; //初期表示件数 - 1
const additions = 10; //追加する件数
let max_id = init_num; //表示している記事の最大インデックス
let start_pos = 0; //現在のスクロール位置
//-----------------
function WatchScrolling() {
const window_top = document.documentElement.scrollTop || document.body.scrollTop;
//上方向にスクロールした場合、処理しない
if (start_pos > window_top) {
start_pos = window_top;
return;
}
start_pos = window_top;
const window_bottom = window_top + document.documentElement.clientHeight;
const target_top = document.getElementById("shirasame-article" + max_id).offsetTop;
if (target_top == null) return;
//一番下に表示されている記事までスクロールしたとき
if (target_top < window_bottom) {
ShowArticles(max_id, max_id + additions);
max_id += additions;
RememberMaxId(max_id);
//最大表示件数に到達した後は処理しない
if (max_id > 48) {
window.removeEventListener("scroll", WatchScrolling);
}
}
}
急にコメントが詳しく書かれ始めました。書かないとどこで何をしているのか忘れそうだと思ったためです。もはやコメントにほとんどすべてが書かれています。
スクロール方向の判定について。tart_posはスクロールする前に表示されている画面の上側の高さです。最初は一番上から表示するので、0にしています。下にスクロールするにつれて数字が大きくなっていきます。対するwindow_topはスクロールした後の画面の上端の高さです。下にいくほど数字が大きくなることから、スクロールした後の高さであるwindow_topがスクロールする前の高さであるstart_posより小さいということは、上方向にスクロールしたということになります。ここから下の処理は、上にスクロールしたときには動かなくていいものなので、つけています。
いつ記事を追加で表示するかの判定について。target_topは今noneのclassがついていない記事のうち、一番下に表示されている記事の上端の高さです。最初であれば、10件目の記事の上端になります。今表示されている画面の下端であるwindow_bottomよりもtarget_topのほうが上にきたとき、つまり一番下の記事が画面に表示されるくらい下にスクロールされたときに、ShowArticles関数を呼び出して、あらたに10件表示するようにしています。その下にRememberMaxIdなる関数もありますね。こちらは4でご紹介します。
途中にあるなぞのnull判定について。「if (target_top == null)」のところは、全部で記事数が50件を超えていないときは、どうしても存在しない記事に対しても処理をおこなおうとしてしまい、エラーをたくさん吐くことになります。それがいやだったのでつけました。50件を超えたあたりで外してしまおうかなと思っています(もう超えてるかも)。いくらエラーを吐いていようとページを見るうえで特に不具合が起こるわけではないので、たぶん好みの問題です。
隠されている記事を表示させる
//記事の表示件数を増やす
function ShowArticles(now_max, next_max) {
for (var i = now_max + 1; i < next_max + 1; i++) {
const article_to_show = document.getElementById("shirasame-article" + i);
if (article_to_show == null) return;
article_to_show.classList.add("fade-in");
article_to_show.classList.remove("none");
}
}
.fade-in {
animation: fadeIn 2s ease 0s 1 normal;
}
@keyframes fadeIn {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
now_maxが今の最大表示インデックス、next_maxが次の最大表示インデックスになります。たとえば今10件表示しているのを増やそうとすると、now_maxが9、next_maxが19になります。でもiをnow_maxに設定してループ処理を回そうとすると、すでにnoneがないところもわざわざ見に行ってしまうので、now_maxに1を足した数値を設定しています。先ほどの例でいうと、インデックスが9の記事も見に行ってしまうため、1を足してインデックス10、つまり11件目からnoneを外していくようにしています。外していくときについでにfade-inというclassもつけます。noneを外すだけだといきなり記事がパッと出てきますが、これを追加することでふわっと表示させることができます。とはいえ、処理に時間がかかったりしてこれをつけていてもパッと表示されてしまうことはよくあることなので、パッと表示されてもあまり気にしないでいただけますとさいわいです。
「if (article_to_show == null)」のところは、さっきのtarget_topと同じ理由でつけています。そろそろ外してもいい気がしています。
ちなみに、「var i = …」のところは、varではなくletでもいいです。最近はvarをあまり使わないのが流行みたいです。流行に乗って、ここもそのうち変えたいと思います。
4. 記事リストにブラウザバックしたときに、さっき表示していたところまでは表示する
window.onload = function () {
SetInitialMaxId();
if (max_id !== init_num) {
ShowArticles(init_num, max_id);
}
}
//Cookieを設定する
function RememberMaxId(value) {
Cookies.remove("shirasame-shown");
Cookies.set("shirasame-shown", value, { expires: 0.0034 }); //約5分
}
//Cookieが取得できたらmax_idに代入する
function SetInitialMaxId() {
if (Cookies.get("shirasame-shown") !== undefined) {
max_id = Number.parseInt(Cookies.get("shirasame-shown"));
}
}
リストをスクロールしている最中に気になる記事を見つけたのでその記事を読みにいって、そこからまた前のページに戻る(リストに戻る)といったことはきっとよくあるだろうと思ったので、この機能を追加しました。js-cookie(GitHub)というプラグインを使っています。
新しく10件表示させるごとに、今表示している件数の最大インデックスmax_idをCookieに保存します。そして、またここに戻ってきたときはまずCookieがあるかを見て、もしCookieがあったなら、そこにあるぶんだけスクロールイベントなしに最初から表示させるという流れです。こうすることで、別のページに行ってからまた戻ってきたときも、さっき表示したところまではすでに表示されることになります。
ちなみに、ここは私が盛大に詰まったのでぜひお伝えさせていただきたいのですが、max_idはintegerつまり数値として扱っているつもりなのですが、Cookieから取ってきた値はstringつまり文字列として扱われます。なので、Cookieから取ってきてmax_idにセットするときはNumber.parseInt()で数値に変換してあげる必要があります。これに気づかなかったために、たとえばCookieから取ってきたmax_idが19で、そこから下にスクロールして新しく10件表示させたときにCookieの値が「1910」とかになったりしました。「max_id + addtions」→「”19” + 10」→「1910」、文字列の結合になってしまったんですね。後ろになぜかつく数字がどうもadditionsらしいということに気づいてからやっと解決しました。長いたたかいでした……
window.onloadはページの読み込みが終わってから処理を始めるものです。これは、js-cookieを読み込み終わる前に動き始めしまうと困るのでつけています。
さいごに
ここまで読んでくださり、ありがとうございました。
さいごにJSの完成図を載せておきます。何かお役に立てればさいわいです。
const init_num = 9; //初期表示件数 - 1
const additions = 10; //追加する件数
let max_id = init_num; //表示している記事の最大インデックス
let timeout_id = 0; //処理を実行するかどうかのフラグ
let start_pos = 0; //現在のスクロール位置
window.onload = function () {
SetInitialMaxId();
if (max_id !== init_num) {
ShowArticles(init_num, max_id);
}
}
window.addEventListener("scroll", SetDelay);
function SetDelay() {
if (timeout_id) return;
timeout_id = setTimeout(function () {
WatchScrolling();
timeout_id = 0;
}, 100);
}
function WatchScrolling() {
const window_top = document.documentElement.scrollTop || document.body.scrollTop;
//上方向にスクロールした場合、処理しない
if (start_pos > window_top) {
start_pos = window_top;
return;
}
start_pos = window_top;
const window_bottom = window_top + document.documentElement.clientHeight;
const target_top = document.getElementById("shirasame-article" + max_id).offsetTop;
if (target_top == null) return;
//一番下に表示されている記事までスクロールしたとき
if (target_top < window_bottom) {
ShowArticles(max_id, max_id + additions);
max_id += additions;
RememberMaxId(max_id);
//最大表示件数に到達した後は処理しない
if (max_id > 48) {
window.removeEventListener("scroll", WatchScrolling);
}
}
}
//記事の表示件数を増やす
function ShowArticles(now_max, next_max) {
for (var i = now_max + 1; i < next_max + 1; i++) {
const article_to_show = document.getElementById("shirasame-article" + i);
if (article_to_show == null) return;
article_to_show.classList.add("fade-in");
article_to_show.classList.remove("none");
}
}
//Cookieを設定する
function RememberMaxId(value) {
Cookies.remove("shirasame-shown");
Cookies.set("shirasame-shown", value, { expires: 0.0034 }); //約5分
console.log("remember " + max_id);
}
//Cookieが取得できたらmax_idに代入する
function SetInitialMaxId() {
if (Cookies.get("shirasame-shown") !== undefined) {
max_id = Number.parseInt(Cookies.get("shirasame-shown"));
console.log("initial " + max_id);
}
}