ajaxfy_comment

プラグインに頼らずテーマ内で完結するAJAXコメントフォーム。

前回に引き続き、コメントの周りのカスタマイズ。

一昔前にQuick Commentsというのがあって、いまはAjax CommentsやWP Ajaxify Commentsというのもあって、でも環境依存の問題か上手く動作しなかったり、もっさりしていたりで、いくつもある他のプラグインの動作確認をやるのもしんどいしで、どうにも…あ゛ーって感じになったので、自作してみたら意外とすんなりまとまった、と。

※2015/2/25 …. undefinedを区別するようにjsを修正しました。このコードはAJAXを使用しているので、htmlタグが誤っていたり、複雑な構造になっている場合、あるいは他のjs系プラグインを使用している場合、干渉しあって期待通りの動作をしないこともあります。十分に注意してお使いください。

 

CODE: Ajaxify WordPress comments

<?php
// コメントフォームのAJAX化
function ajaxify_comments_script(){
if ( is_singular() && comments_open() )://固定や投稿等シングルページで動作、場所によっては要変更
?>
<script id="comment-script" type="text/javascript">
jQuery(function( $ ){

    var commentbox_name = '#comments', //コメントテンプレート名、カスタマイズ時は要変更
        commentform_name = '#commentform', //コメントフォーム名、カスタマイズ時は要変更
        commentbox = $( commentbox_name ),
        commentform = $( commentform_name, commentbox );
    ajaxCommentsTrigger();

    function ajaxComments( this_form ){
        var statusdiv = $('<div class="comment-ajax"></div>'); // AJAXの結果を吹き出す窓、デザインはCSSで指定してね
        this_form.append( statusdiv ).submit( function (e) {
            var formdata = this_form.serialize(), // フォームをシリアライズ
                formurl = this_form.attr('action'); // フォームのactionに設定されたurl
            e.preventDefault();
            statusdiv.html('<div class="ajax-processing">コメントを送信中…</div>'); // 送信されると.comment-ajax内に表示
            $.ajax({ 
                type: 'post',
                url: formurl,
                data: formdata
            }).done(function(data,status){
                if( status == "success" ){
                    var matches = data.match(/<body+s+id="error-page">([sS]*?)</body>/);
                    if ( matches ) {
                        var output = $('<div class="ajax-error">' + matches[1] + '</div>');
                        output.find('*:empty').remove();
                        output.find('*:first-child').siblings().remove();
                        statusdiv.html( output );
                        return false;
                    }
                    var newlist = $( data ).find( commentbox_name );
                    if ( newlist.length ){ 
                        statusdiv.html('<div class="ajax-success">コメントありがとうございました。</div>');
                        reply = this_form.parents( 'li.depth-1' ).attr( 'id' );
                        if ( typeof reply !== "undefined" ) {
                            window.location.hash = reply;
                            statusdiv.find( '.ajax-success' ).stop().delay( 500 ).slideUp( 200,function(){
                                commentbox.replaceWith( newlist );
                                commentbox = $( commentbox_name );
                                ajaxCommentsTrigger();
                                $('html, body').animate({
                                    scrollTop: $( '#' + reply ).offset().top
                                }, 500);
                            });
                        }
                    } else {
                        statusdiv.html('<div class="ajax-error">エラーが発生しました。ウェブマスターにご連絡ください。</div>');
                        return false;
                    }
                } else {
                    statusdiv.html('<div class="ajax-error">エラーが発生しました。時間をおいて再度送信して下さい。</div>');
                    return false;
                }
            }).fail(function(data){
                statusdiv.html('<div class="ajax-error">送信が中断されました。<strong>記入もれや間違いがないか</strong>再度ご確認ください。</div>');
            });
        });
    }

    function ajaxCommentsTrigger(){
        var replybox = $( '<div id="reply-box"></div>' ),
            commentform = $( commentform_name, commentbox );
        commentform.clone( true ).appendTo( replybox );
        replybox.find( '[id]' ).each(function(){
            $(this).prop('id', 'reply-' + $(this).attr( 'id' ) );
        });
        ajaxComments( commentform );
        ajaxComments( replybox.children( 'form' ) );
        $( 'a.comment-reply-link', commentbox ).click(function(e){ 
            e.preventDefault();
            var replylink = $(this), list = replylink.parents( 'li.depth-1' );
            if ( list.hasClass( 'replying' ) ){
                list.removeClass( 'replying' );
                replylink.html( '返信する' );
                replybox.slideUp( 500 );
            } else {
                list.addClass( 'replying' );
                list.siblings('.replying').removeClass( 'replying' ).find( 'a.comment-reply-link').html( '返信する' );
                replylink.html( '返信をキャンセル' );
                var parentid = list.attr('id').slice(8); // ex:#comment-999
                replybox.find( '#reply-comment_parent' ).val( parentid );
                replybox.hide().appendTo( list ).slideDown( 500 );
            }
        });
    }
});
</script>
<?php
endif;
}
add_action('wp_footer', 'ajaxify_comments_script');
?>

このサンプルコードではfunctions.phpに記載することを前提にwp_footerにフックしてますが、jQueryだけで完結してるんでjs部分をテンプレートに直書きしてもOKです。動作はここのフォームでコメントなしで送信ボタンを押せば、エラーが返ってきますのでどうぞ。ちなみに前記事「Really Simple CAPTCHAをコメント欄に応用する。」も併用できます。

 

どうやってヴァリデートするか、が肝なのかも。

ajaxでpostするところまではどのプラグインでも動作は大体同じ。コメントのヴァリデーション方法については作者毎に個性があるが、comment_postにフックしてajax時のみ処理を分岐させて云々…というのが常道のようだった。それで良いのだけども、結局、コメント投稿に失敗したときに返ってくるエラーページを読み込んでPOSTの可否を判断することにした。htmlソースで判断することの是非はあるかもしれないが、フック処理が不要になるしwordpressのエラーメッセージを流用できるので、関数をシンプルに保てて、保安上もメリットになると思えたからだ。

もうひとつの大きな設計判断としては、コメント後にユーザーにだけ見えるモデレート待ち のコメントを表示する方法をどうするかだけども、これはコメントが通過後に返ってくる新しいポストから#commentsタグを抽出して丸ごと差し替えること にした。キャプチャを使用している場合にはその更新が必要だったり、コメントテンプレートのデザインやソートがテーマ毎にまちまちだったりするので、コメントテンンプレートをまるごと差し替えるのが現実的だろうというわけです。

おまけ:ここで適用してるcss

ajaxは思いの外時間がかかることもあるので、css 3のアニメーションなどを活用してユーザーを迷わせないよう設計することが大事ですね。

.comment-ajax { margin:5px 0; }
.comment-ajax > div { padding:5px 10px; font-size:16px; }
.ajax-error { background:#356; color:#fff; }
.ajax-success { background:#2bb; color:#fff; } 
.ajax-processing { position:relative; background:#eee; } 
.ajax-processing:before { width:100%; display:block; content:' '; margin:0; padding:0; background:#a9e3e4; position:absolute; height:3px; left:0; bottom:0; -webkit-animation:fullexpand 1.5s ease-out; z-index:1; animation:fullexpand 1.5s ease-out; }
@-webkit-keyframes fullexpand { 0%  { width:0;} 100%{ width:100%;} }
@keyframes fullexpand { 0% { width:0;} 100%{ width:100%;} }
first_time_user_or_not

特定ページへの初回訪問かどうかを見分ける

jQueryなどを使って初回訪問時のみアクションを加えたい。でもクッキーなんて使いたくない。

…という場合には、ウインドウ名を利用して簡易的に判断する方法が有効です。javascriptで表示中のURLからファイル名(ベースのスラッグ)を取得し、ウインドウ名に加える処理を行います。

verify first time visitor or not

<script type="text/javascript">

    //verify first time visitor or not in the pages
    var $currentpath = window.location.pathname.replace(///, '').replace(/./, '').replace(/_/, '-'); // 表示中のページのファイル名を取得する
    if ( !$currentpath ) $currentpath = "homepage"; // トップページ(ファイル名が存在しない場合)に登録する任意のファイル名
    if ( window.name.indexOf( '#' + $currentpath )!=-1 ) { //ウィンドウ名に現在のファイル名が含まれるかチェック
        alert( "I've seen you before!" ); // リロードや2度目以降の訪問時の処理
    } else { 
        window.name = window.name + '#' + $currentpath; // ウィンドウ名の末尾にファイル名を追加する、#はエスケープ処理
        alert( "It's nice to see you!" ); // 初回訪問時の処理
    }

</script>

チェックしたいページ全てに記述する必要があります。ウインドウ名を利用しているので別窓で開くと無効になります。

なお、ウインドウ名には文字数制限(255文字かな?)があるはずなので、動的サイトで全体のアクセス履歴を書き込んでいくのは無理があります。また、ウィンドウ名に指定できるのは半角英数字のみです。なのでwordpressなどのCMSに埋め込むなら、トップページや固定ページ内だけなどに絞って、何かアクションさせたいときに使いましょう。wordpressなどのCMSで埋め込むなら、ページテンプレートやホームテンプレート内で使用し、らpathnameを取得する手続きをカットし、直接phpでpost IDを使った名前を代入する方が、処理も早くて文字数も抑えられます。以下はWPで実際に私が使ってる感じに近いもの。

var currentid = <?php echo "'p$post->ID#'"; ?>; // #はエスケープ処理
if ( window.name.indexOf( currentid ) == -1 ) { // 初アクセス 
    window.name = window.name + currentid; // ウィンドウ名の末尾にファイル名を追加する
    //以下処理が続く
}

 

そもそもユーザーがホームページへの初回訪問かどうかを判別する場合

おまけ。

ニュースなどのウインドウをポップアップしたいときなど、すべてのページでユーザーが初回訪問かどうか有効にしたいときもあります。この場合、チェックしたい各ページ全てに記述する必要があります。

<script type="text/javascript">

    //verify first time visitor or not in the site
    var $my_window = "my_homepage"; // 任意のウィンドウ名を指定
    if ( window.name.indexOf( $my_window )!=-1 ) { //任意のウインドウ名かチェック
        alert( "I've seen you before!" ); // 2度目以降の訪問時の処理
    } else { 
        window.name = $my_window; // ウィンドウ名を任意に名付ける
        alert( "It's nice to see you!" ); // 初回訪問時の処理
    }

</script>

とにかく外部からのアクセス時の一回だけでいい、という場合は以下のようにシンプルに処理する方法もあります。リファラを見ているので、別ウインドウで開いても大丈夫ですし、該当するページ一箇所に記述するだけで済みます。しかしスクリプトの設置の有無で判別できないので、サブディレクトリ以下でのみチェックをしたい場合には不向きです。

<script type="text/javascript">

    //verify first time visitor or not in the site
    
    if( document.referrer.indexOf( "//example.com" )!=-1 ) {  { //リファラにアドレスが含まれているか。
        alert( "I've seen you before!" ); // 2度目以降の訪問時の処理
    } else { 
        alert( "It's nice to see you!" ); // 初回訪問時の処理
    }

</script>
imagelightbox-js

//osvaldas.info/image-lightbox-responsive-touch-friendly

 

軽くてシンプルでカスタマイズしやすいレスポンシブでタッチフレンドリーなライトボックス

ほとんどタイトルで言い切ってしまったのだけども、Image LightboxはデフォルトではCSSは何も指定されていないというシンプルなライトボックス系のjQueryプラグイン。エフェクト開始、画像読み込み、読み込み終了、エフェクト終了、等々コールバックが充実しているので、ウェブデザインや用途に合わせてとてもカスタマイズしやすい作りになっている。ライトボックスが珍しかった時代はともかく、ユーザーがウェブ体験に慣れている現在、余計なUIがない方が扱いやすいのではないかと思う。

簡単な設定

使い方は簡単でまずはjsを読み込みます。

<script src="jquery.js"></script>
<script src="imagelightbox.js"></script>
<script>
    jQuery( function($){
        $( 'a' ).imageLightbox();
    });
</script>

次にcssに以下を設定

#imagelightbox{
    position: fixed; z-index: 9999;
    -ms-touch-action: none; touch-action: none;
}

以上です。

が、しかし…

Image Lightboxはie8に対応していないという、ブラウザ島国事情にとってあまりに厳しい条件。なんとかして使いたいと思ったので、以下ではie8対応にする方法を考えることにします。(作者のValutisさんはリトアニアの方のようだけども、レガシーブラウザは死滅しているみたい…めちゃめちゃうらやましい。)

 

ImageLightbox.jsをIE8対応に変更する

やってみたら主な(すべてではない)障害は二つあった。

parseIntで整数値が返ってこない

一つはparseIntの対象となる要素(デフォルトだとimg#imagelightbox)のcssでleftの値がエラーを起こすことで、cssに値を与えれば良いのだが、それだけでは中央揃えも出来ない有様なのでcssハックで中央揃えすることに。

img#imagelightbox { position:fixed; z-index:9999; -ms-touch-action:none; touch-action:none; } /* minimal css */
img#imagelightbox { left:09; right:09; top:09; bottom:09; margin:auto9; } /* IE8 hack */

load functionが発火しない

これで本体に手を付けずに済んだ…と思ったのもつかの間、いったん機嫌良く表示された画像を閉じ、再度リンクを押して表示しようとすると画像が読み込まれないという。これはあれだ。たいへん残念なIEらしい、画像がキャッシュされてしまうとload functionが発火しないという致命的なバグ。139行下のload function手前のjQueryでsrcを付与している処理、これをfunctionから切り離して後述するように変更。

元のコード

image = $( '<img ' + options.selector + ' />' )
.attr( 'src', target.attr( 'href' ) )
.load( function()
{
image.appendTo( 'body' );
setImage();
/////////// 略 ///////////
})
.error( function()
{if( options.onLoadEnd !== false ) options.onLoadEnd();
})

これを次のように変更してやります。

image = $( '<img ' + options.selector + ' />' ).appendTo( 'body' );
image.load( function()
{
setImage();
/////////// 略 ///////////
})
.error( function()
{if( options.onLoadEnd !== false ) options.onLoadEnd();
});
image.attr( 'src', target.attr( 'href' ) );

改変したコードはここにあげときます。(コードを改変したくなければ、ie8の場合のみリンク末尾にjsでタイムスタンプを付ける、という手法でも回避できるのではないかと思います。未検証ですが。)

sc 2014-10-05 22.44.54

メディアアップローダ周りのデフォルト表示設定

メディアの量が増えてくると、いちいちロード時間が重っ苦しい。些細なことだけども、ユーザーにとって小さな親切大きな優しさというもの。

WP3.5からメディアアップローダーが新しくなり、「この投稿へのアップロード」をデフォルト表示を設定するのにjQueryを使ってajaxを検知しアップローダー読み込み後にセレクターを指定する手法が知られてました。(Change the default-view of Media Library in 3.5?)私もずっとそうしていたのだけども、最近、セレクトボックスを他のものに指定して作業しようとすると「この投稿へのアップロード」に戻されてしまうことに気がついた。どうやらajaxが複数回呼ばれるようになったためにjQueryが重複して読み込まれている模様。

※wp4.7.2(4.7.1?)よりprototypeアップローダーを呼び出す際にon(‘ready’,function(){})だとなぜかオーダーが早すぎることになったようです。詳しくは追いかけてませんが、on(‘open’,function(){})に変更したところ、問題なく稼働するようです。以下も修正しておきました。(2017.2.4)

他にwp.mediaのデフォルトのselect拡張自体を上書きする方法もあったのですが、これも最近になってうまく動作していないようでした。自作プラグインで使う必要もあって試行錯誤の末に、ajaxの検知をトリガーにするのをやめてアップローダーに紐づけしなおすことにしました。この機に他のデフォルト設定共々備忘録としてまとめておきます。問題点や不具合があれば教えて下さい。

「この投稿へのアップロード」をデフォルト表示

wp.media.view.Modal.prototype.on( 'open', function( ){ $( 'select.attachment-filters' ).find( '[value="uploaded"]').attr( 'selected', true ).parent().trigger('change'); });

メディアアップローダのデフォルト表示を「全てのメディア」から「この投稿へのアップロード」に変更します。

「この投稿へのアップロード」のみを表示

wp.media.view.Modal.prototype.on( 'open', function( ){ $( 'select.attachment-filters [value="uploaded"]').attr( 'selected', true ).siblings().hide().parent().trigger('change');});

いっそ、メディアアップローダのデフォルト表示を「この投稿へのアップロード」しか表示しないようにします。

「ファイルをアップロード」タブの優先表示

wp.media.controller.Library.prototype.defaults.contentUserSetting=false;

「メディアライブラリ」よりも常に「ファイルをアップロード」タブを優先表示するようにします。

左サイドメニューを非表示に設定

wp.media.view.Modal.prototype.on( 'open', function( ){ $( '.media-frame' ).addClass( 'hide-menu' ).addClass( 'hide-router' ); });

「ギャラリー」などのタブがいらない、という場合は左のナビを丸ごと削除します。
※上記のコードは「ファイルをアップロード」タグも隠すようになっています。ドラッグドロップでアップロード可能ですが、クライアントにとっては明示的で内分不親切かもしれませんので、その場合は上記の” .addClass( ‘hide-router’ ) “となっている箇所を削除してください。

 

CODE: Change the default Media Uploader view in WP4.0

<?php

function media_uploader_default_view() { 
?>
<script type="text/javascript">jQuery(function( $ ){ 
// メディア挿入セレクタのデフォルト表示を「この投稿へのアップロード」に設定
wp.media.view.Modal.prototype.on( 'open', function( ){ $( 'select.attachment-filters' ).find( '[value="uploaded"]').attr( 'selected', true ).parent().trigger('change'); });

// メディア挿入セレクタを「この投稿へのアップロード」のみの表示に設定
wp.media.view.Modal.prototype.on( 'open', function( ){ $( 'select.attachment-filters [value="uploaded"]').attr( 'selected', true ).siblings().hide().parent().trigger('change');});

// 「ファイルをアップロード」タブを優先表示
wp.media.controller.Library.prototype.defaults.contentUserSetting=false;

// 左サイドメニューを非表示に設定 
wp.media.view.Modal.prototype.on( 'open', function( ){ $( '.media-frame' ).addClass( 'hide-menu' ).addClass( 'hide-router' ); });

});
</script>
<?php }
add_action( 'admin_footer-post-new.php', 'media_uploader_default_view' );
add_action( 'admin_footer-post.php', 'media_uploader_default_view' );

?>

以上、必要なスクリプト以外は削除するかコメントアウトしてください。

 

その他のメディア周りで使える便利な設定

メディア表示をアップロードしたユーザーのみに限定する

挿入画像のデフォルト設定の変更(Insert Media – Attachment Display Settings: set a default)