Bloggerのコメント欄を標準jsなしで返信可能にする

はじめに

Bloggerのコメント欄改善です。標準jsを読み込めば、返信(リプライ)できるようにできます。ですが、できるだけスクリプトを使用したくないため、自作して最小限のスクリプトで済ますようにします。

仕様

  • コメントをJavaScript無効で表示する
  • コメントに対して返信できる
  • コメントの返信に対して返信できない
    • 既にコメントの返信の返信が存在する場合、表示されない
  • コメントの表示は、200件までそれ以上はコメント不可とする
    • Bloggerは、200件以上コメントできるが後述する問題があるため、制限する
    • 既に200件を超えるコメントがある場合、201件目から表示されない
  • 返信コメント欄は、返信ボタンでコメントフォームを返信元直下まで移動する

※ページ下部のコメント欄参照

動作原理

テンプレートで返信を表示する原理

Bloggerのコメントは、コメント日時順で並んでいます。コメントと返信が連続しているわけではありません。なので、where文を使用して、inReplyTo(返信元のID)があるかないかでループしています。

inReplyToなし(返信以外)を列挙しつつ、対象のコメントへの返信を続けて書き込んでいます。

<b:loop values='data:post.comments where (c => not c.inReplyTo)' var='comment'>
  ...
    <b:loop values='data:post.comments where (c => c.inReplyTo == data:comment.id)' var='reply'>
      ...
    </b:loop>
    ...
</b:loop>

200件以上のコメント

200件制限の理由。上記の通りBloggerのコメントは、日付順に並んでいます。そして、1度に取得できるコメント数は200件です。200件を超えるとページングが必要になります。ただ、ここで、200件以上ある場合、返信が200件未満のコメントに付けられると表示元のコメントが見つからず表示できなくなります。問題を解決できないため、コメント制限と言う方法で回避しています。

※通常のブログで200件以上のコメントが付くことは稀なため、問題ないと判断しています
※コメント200件付いたことがないため、テストしてないです
<data:post.commentHtml/>は、どうやってこの問題を解決しているのかな?

返信ボタン

返信ボタンは、最後の投稿のフッター部に配置しています。返信先はもちろん元コメントです。返信コメントではありません。(返信コメントに返信してしまうと、上記の2重ループでは対応できず、3,4重ループが必要になってきます)

スクリプト

スクリプトには、遅延読込みの一部が組み込まれています。画面内にコメント領域が入るとコメントエディタを読込みに行きます。また、初期化時に非表示だった返信ボタンを表示します。

※本番環境では、onLazy.jsで更に遅延処理をかけています

コード

.clearfix::after {
  content: '';
  display: block;
  clear: both;
}
.noscript {
  color: #B36675;
  font-weight: 700;
}
.comment-block {
  padding: 1em;
  border-bottom: 1px solid #e6e6e6;
}
.comment-reply-block {
  margin: 1em 0 0;
  padding: 1em 0 0 2em;
  border-top: 1px solid #e6e6e6;
}
.comment-content {
  margin: 1em 0;
}
.comment-author,
.comment-deleted {
  font-weight: bold;
}
#comment-form-block {
  padding: 1em;
}
#comment-editor {
  width: 100%;
  height: 262px;
  border: none;
  /* 通常:210px, プレビュー247px, 未ログイン:261px, モバイル:277px, モバイル未ログイン:354px */
}
:not(#comment-form-block) > #comment-editor {
  height: calc(262px + 1em);
  margin: 1em 0 0;
  padding: 1em 0 0;
  border-top: 1px solid #e6e6e6;
}
.comment-reply {
  float: right;
  padding: .25em .5em;
  border: 1px solid #c6c6c6;
  background: #f8f8f8;
}

<div id='comments'>
  <b:if cond='data:post.allowComments'>
    <h2>コメント</h2>

    <b:if cond='data:post.numberOfComments != 0'>
      <b:loop values='data:post.comments where (c => not c.inReplyTo)' var='comment'><div class='comment-block' expr:id='data:comment.anchorName'>
        <div class='comment-author'>
          <b:if cond='data:comment.authorUrl'>
            <a class='comment-author-name template-link' expr:href='data:comment.authorUrl' rel='nofollow'><data:comment.author/></a>&amp;nbsp;さんのコメント...<svg class='icon'><use xlink:href='#icon-comment'/></svg>
          <b:else/>
            <span class='comment-author-name'><data:comment.author/></span>&amp;nbsp;さんのコメント...<svg class='icon'><use xlink:href='#icon-comment'/></svg>
          </b:if>
        </div>
        <div class='comment-content'>
          <b:if cond='data:comment.isDeleted'>
            <p class='comment-deleted'><data:comment.body/></p>
          <b:else/>
            <p><data:comment.body/></p>
          </b:if>
        </div>
        &lt;div class='comment-footer' class='clearfix'&gt;&lt;div&gt;<a class='template-link' expr:href='data:comment.url' rel='nofollow ugc'><data:comment.timestamp/></a>
        <b:loop values='data:post.comments where (c => c.inReplyTo == data:comment.id)' var='reply'>
          &lt;/div&gt;&lt;/div&gt;
          &lt;div class='comment-reply-block' id='<data:reply.anchorName/>'&gt;
          <div class='comment-header'>
            <b:if cond='data:reply.authorUrl'>
              <a class='comment-author template-link' expr:href='data:reply.authorUrl' rel='nofollow'><data:reply.author/></a>&amp;nbsp;さんのコメント...<svg class='icon'><use xlink:href='#icon-reply'/></svg>
            <b:else/>
              <span class='comment-author'><data:reply.author/></span>&amp;nbsp;さんのコメント...<svg class='icon'><use xlink:href='#icon-reply'/></svg>
            </b:if>
          </div>
          <div class='comment-content'>
            <b:if cond='data:reply.isDeleted'>
              <p class='comment-deleted'><data:reply.body/></p>
            <b:else/>
              <p><data:reply.body/></p>
            </b:if>
          </div>
          &lt;div class='comment-footer' class='clearfix'&gt;<a class='template-link' expr:href='data:reply.url' rel='nofollow ugc'><data:reply.timestamp/></a>
        </b:loop>
        <button class='comment-reply' type='button' hidden='1' expr:data-id='data:comment.id'>返信</button>&lt;/div&gt;&lt;/div&gt;
      </div></b:loop>
    </b:if>

    <b:comment>コメント投稿</b:comment>
    <div class='clearfix' id='comment-form-block'>
      <b:if cond='data:post.commentPagingRequired'>
        <div class='comment-form'>コメントの最大数に達しました。これ以上書き込めません。</div>
      <b:elseif cond='data:post.embedCommentForm'/>
        <b:if cond='data:post.allowNewComments'>
          <button class='comment-reply' id='comment-form-undo' type='button' hidden='1' data-id='0'>新規</button>
          <noscript><span class='noscript'>コメントを書き込むには、JavaScriptを有効にする必要があります。</span></noscript>
          <iframe allowtransparency='true' id='comment-editor' role='form' src='' expr:data-src='data:post.commentFormIframeSrc' loading='lazy' hidden='1'/>
        <b:else/>
          <data:post.noNewCommentsText/>
        </b:if>
      <b:elseif cond='data:post.allowComments'/>
        <div class='comment-url'><a class='template-link comment-url-btn' expr:href='data:post.addCommentUrl' expr:onclick='data:post.addCommentOnclick'><span><data:postCommentMsg/></span></a></div>
      </b:if>
    </div>
  </b:if>
</div>

(function(window, document) {
  // リプレイ用
  const replyTo = function() {
    const id = this.dataset.id != '0' ? this.dataset.id : 0;
    const editor = document.getElementById('comment-editor');
    const src = editor.dataset.src + (id ? '&parentID='+id : '');
    if (src == editor.src) {
      return;
    }
    const m = editor.src.match(/&parentID=(\d+)/);

    // コメントフォーム更新(表示)
    editor.onload = function() {
      this.style.visibility =  'visible';
    };
    editor.style.visibility = 'hidden';
    editor.src = src;

    // コメントフォーム移動
    const containerid = id ? 'c'+id : 'comment-form-block';
    document.getElementById(containerid).insertBefore(editor, null);
    document.getElementById('comment-form-undo').hidden = !id;

    // 返信ボタン有効無効切替え
    m  && (document.querySelector('#c'+m[1]+' .comment-reply').disabled = false);
    id && (document.querySelector('#c'+id+' .comment-reply').disabled = true);
  };

  // コメントフォームの読み込み
  const onLoadCommentForm = function() {
    // コメントエディタを読込む
    const editor = document.getElementById('comment-editor');
    if (editor && editor.src != '') {
      editor.addEventListener('load', function() {
        editor.hidden = false;
        editor.style.display = 'block';
        // uBlockに`display:none;`にされることがある

        // リプレイを準備
        const replies = document.querySelectorAll('.comment-reply');
        for (let i=0; i<replies.length; i++) {
          if (replies[i].dataset.id != '0') {
            replies[i].hidden = false;
          }
          replies[i].addEventListener('click', replyTo);
        }
        // 補足:コメントエディタ読込みに失敗する可能性がある
        //       (トップドメインのみスクリプトが許可されている場合)
      });
      editor.src = editor.dataset.src;
    }
  };

  function main() {
    // コメントフォームボタンが領域内に入った場合、コメントフォームを読み込む
    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function(entries, observer) {
        // 登録後、intersectionRatio=0で一旦発火する
        if (entries[0].intersectionRatio != 0) {
          onLoadCommentForm();
          observer.disconnect();
        }
      }, {rootMargin:'512px'}).observe(document.getElementById('comments'));
    } else {
      onLoadCommentForm();
    }
  };

  main();
  //window.addEventListener('lazy', main);
})(window, document);

※FontAwesomeのアイコン、コメント関連以外のclass、遅延処理などがページ下部の実物とは異なります

補足

日付の新しい順に並べ替えることもできます。(ループを逆順にまわしています)

<b:loop reverse='true' values='data:post.comments where (c => not c.inReplyTo)' var='comment'>

次のdataタグで簡単にコメントのHTML出力できます。デザインの独自性気にしないならぜひ。(形式が異なるため、上記スクリプトは動作しません)

<data:post.commentHtml/>

参考

関連