bodyとpostクラスに所属カテゴリのスラッグを付与したい。

だいぶ前に「投稿が所属するカテゴリの先祖や子孫を取得するあれこれ。」という記事を書きましたがそこそこアクセスがあったので、実用レベルでよく使うアレンジも紹介しておきます。functions.phpに入れるだけで動きます。正確には、使用テーマのテンプレートでbody_classとpost_classを使用していれば動きます。

実際の出力例は以下のような感じです。

アーカイブページを表示中の場合

<body class=”archive category category-child category-99 logged-in admin-bar no-customize-support category-ancestor category-parent“>

投稿ページを表示中の場合

<body class=”single single-post postid-999 single-format-standard logged-in admin-bar no-customize-support category-child category-ancestor category-parent taxonomy-term“>

category-childが表示中のカテゴリだとすると、category-ancestor(先祖カテゴリ)やcategory-parent(親カテゴリ)も付与してくれるのがミソです。すべてのタクソノミに対応するようにしています。cssによるカスタマイズがしやすくなるのではないでしょうか。もちろんカスタムタクソノミにも対応です。

※ちなみに以下の関数はpage slugにも対応していますが、不要な場合は削除してください。すでに同じ機能の関数を設置している場合はそっちを削除するか、こっちに変更を加えてください。

CODE


/////////////////////// 投稿及びカテゴリページで所属カテゴリ・親カテゴリクラスを付与 ///////////////////////

function add_term_name_to_body_and_post_class( $classes ) {

if ( is_admin() ) return $classes; //管理画面を除く

if ( is_singular() ){ //シングル投稿タイプ

global $post;
$capability = get_post_type_object( $post->post_type )->capability_type;

if ( $capability == 'page' ){ // 固定ページ及びカスタム固定ページ // ページスラッグ付与が不要なら以下の分岐を削除

  $classes[] = $post->post_type.'-' . $post->post_name;
  if ($post->post_parent) $classes[] = $post->post_type.'-' . get_page_uri($post->post_parent) . '-child';

} elseif ( $capability == 'post' ){ // 投稿及びカスタム投稿タイプ

  $taxes = get_object_taxonomies( $post->post_type, 'names' );
  if ($taxes) : unset( $taxes['post_format'] );
  foreach ( $taxes as $taxname ) :
    $tax = get_taxonomy( $taxname );
    if ( !$tax || !$tax->hierarchical ) continue;
    $terms = get_the_terms( $post->ID, $taxname );
    if ( $terms ): foreach ( $terms as $term ) :
      $classes[] = $taxname. '-' .$term->slug;
      $parent = get_term_parent_slug_by_slug( $term->slug, $taxname );
      while( $parent ){ //loop
        $classes[] = $taxname. '-' .$parent;
        $parent = get_term_parent_slug_by_slug( $parent, $taxname );
      }
    endforeach; endif;
  endforeach; endif;

}

} elseif ( is_archive() ) {

  $term_slug = $taxname = '';
  if ( get_query_var( 'category_name' ) ){

    $term_slug = get_query_var( 'category_name' );
    $taxname = 'category';

  } elseif ( get_query_var( 'taxonomy' ) && get_query_var( 'term' ) ) {

    $term_slug = get_query_var( 'term' );
    $taxname = get_query_var( 'taxonomy' );

  }

  if ( $term_slug && $taxname ){

    $parent = get_term_parent_slug_by_slug( $term_slug, $taxname );
    while( $parent ){ //loop
    $classes[] = $taxname. '-' .$parent;
    $parent = get_term_parent_slug_by_slug( $parent, $taxname );
  }

}

}
return $classes;

}
add_filter( 'post_class', 'add_term_name_to_body_and_post_class' );
add_filter( 'body_class', 'add_term_name_to_body_and_post_class' );

 

escaping

Advanced Custom Fieldsの便利さを享受しながら、クローズド化の罠を回避する

Advanced Custom Fields(以下ACF)が世に出てから4年は経つでしょうか。いくつかスマートげなプラグインも出たものの、いまだにシェアは圧倒的。今ひとつよくわからん機能があっても、管理・入力補助にすぐれ、結局クラインアントワークでは欠かせないんですよね。

しかし出力に独特の癖があるACF。この先、プラグインの乗り換えを検討する時が来るかもしれません。カスタムフィールドはWPで今後も利用されていくでしょうが、ACFに依存するテーブルデータや出力関数があるせいでプラグインやテーマを互換性がないなんて、オープンプラットフォームを使っている意味がないと言っても過言じゃない。そこで独自関数やテーブルを利用しないで済ませる方策を考えていきます。

カスタムフィールドの値はできるだけIDを持たせるようにする

ファイルやページなどをカスタムフィールドに登録する場合は、URLやオブジェクトを指定するとファイルやページに変更があったときに支障が出ます。そもそも、wordpressのデフォルト関数などでは保持される値はそのほとんどがIDか単純な文字列です。直感的に分かりやすいとメンテナンス性も上がりますし、汎用性を考えてもファイルや関連記事などの返り値はなるべくIDにしておく方がいいでしょう。

sc 2016-06-14 17.08.55

チェックボックスなど複数の値を持つvalueを出力する

WPでは同じキーを持つカスタムフィールドを複数登録できますが、ACFはチェックボックスやセレクトボックス、関連記事などのフィールドは複数の値を配列のままストアされます。プラグインが配布された当初から指摘されていたように、ACFのシリアライズされた値の保持方法はmeta queryや検索との相性がよくありません。しかしqueryに載せるならwp3.0以降はカスタムタクソノミーが設定できますし、その方がパフォーマンスもいいですので、シリアライズされることの一番の問題は抽出方法ということになるかと思います。(余談ですが、WP自体はカスタムフィールドに文字列以外が入ることも想定した設計になっていますし、配列を持たせることで判定と出力がシンプルにできるので、一概にダメな設計ではないように思います。)

さてさて、この複数値を出力する方法としては、ほとんどのサイトが独自関数get_field(またはthe_field)を使うように案内しています。

// 従来のACFの配列を出力するコード
$fields = get_field('field');
if($fields): foreach($fields as $val): 
   echo $val; 
endforeach; endif

…でもなんのことはない、値が配列になっているだけなので、以下のようにすれば出力ます。これによってテンプレートはユニバーサルな状態に保っておくことができます。

get_post_metaを使って配列を出力する

$fields = get_post_meta( $post->ID, 'field', true );
if ( $fields ): foreach( $fields as $key => $val ):
 echo $val;
endforeach; endif;

 

Repeater fieldを独自関数を使わずにアウトプットする

初めて有料で購入したWPのプラグイン拡張だった様な気がします。25$。これは買ってください。
sc 2016-06-14 8.33.26

相当数の人が使用していると思われるリピーターフィールドですが、これも出力方法に癖があります。公式に案内されているのは、次のような感じです。ゴリゴリのプラグイン依存ですね。

// 従来のrepeater fieldを出力するループ関数
if( have_rows('field') ): while( have_rows('field') ): the_row();

  $subfield = get_sub_field('subfield'); 
  // do something with subfield

endwhile; endif;

これは以下のようにPHPとget_post_metaで表現することが出来ます。こうすればデータベースに値があれば、 ACFなしで動作します。何を求めているコードなのかPHP的に一目瞭然なのが精神衛生上いいですね。

get_post_metaとforループで配列を出力する

$n = get_post_meta( $post->ID, 'field', true);
if ($n): for ($i = 0; $i < $n; $i++ ):
  $subfield = get_post_meta( $post->ID, 'field_'.$i. '_subfield', true );
  // do something with subfield

endfor; endif;

実地に合わせるとこんな感じに落ち着きます。

$n = get_post_meta( $post->ID, 'item', true);
if ($n): for ($i = 0; $i < $n; $i++ ):
  $image = get_post_meta( $post->ID, 'item_'.$i. '_image', true );
  if ( $image ) {
    echo '<div class="image">'.wp_get_attachment_image ( $image, 'medium' ).'</div>'."n";
  } 
  $text = get_post_meta( $post->ID, 'item_'.$i. '_text', true ); 
  if ( $text ) {
    echo '<div class="text">'.wpautop( $text ).'</div>'."n";
  }
endfor; endif;

カテゴリのカスタムフィールドをwp term meta対応にする

カテゴリに画像や注釈を付与するという要件は、CMSではよくある話です。カテゴリのカスタムフィールド設定は、なかなか満足できるプラグインがなくACFを使わざるをえず、ACFの独自データテーブルに依存するのを避けられませんでした。しかしwp4.4から公式にカテゴリやタグなどのタームはデフォルトでメタフィールド(wp term meta)が設置可能になりました。当初すぐにACFもサポートするものだと思っていましたが、半年待っても音沙汰なく、今後対応するかどうかも一切不明。wp term metaに対応すると、ユーザー囲い込みの一端が解けることを嫌ってるんじゃないかとか邪推したくなります。公式の機能からかけ離れていくのもどうかと思うんですけどね。

で、ACFのフォーラムで素晴らしい発案(How to use WP Term Meta, the easy way!)があったので私もそれを採用することにしました。ACF側でタームメタが更新された時に、自動的にwp term metaを上書きするだけです。逆方向はどうするんだとか、疑問はあるかもしれませんが、基本的には、wp term metaにストアさえされていれば、get_fieldを使わずに公式のget_term_metaで出力できまので大抵のテーマでは間に合うでしょう。これでいざACFとの離婚協議が始まっても、優位にたてるというわけです。

以下をfunctions.phpに書き込みます。

Advanced Custom Fieldsのタームメタ変更に合わせてwp_term_metaを上書き

add_filter( 'acf/update_value', 'acf_update_term_meta', 11, 3 );
function acf_update_term_meta($value, $post_id, $field) {
    $term_id = intval(filter_var($post_id, FILTER_SANITIZE_NUMBER_INT));
    if($term_id > 0) update_term_meta($term_id, $field['name'], $value);
    return $value;
}

たとえばカテゴリに画像IDを登録したとして、それをテンプレートから出力する際は、以下様になります。wp_term_metaを呼び出す、つまりwpのデフォルト関数を使うだけ。シンプルですね。

$term_img = get_term_meta( $term_id, 'category_image', true );
if( $term_img ) {
  echo wp_get_attachment_image ( $term_img, 'thumbnail' );
}

 

WP_DEBUGをオンにすると、akismetによって大量のスパムコメントチェックなどがdebug.logに記載されていく。もうログ自体がスパムみたいになってますよ。1年ほど前にakismetのプラグインにフィルターが付加されていたことに最近気がつきました。これでakismetだけログへの書き込みをオフに出来ます。

###0###

プラグインの機能拡張に伴い自動的にoptionの値を更新したい、ということがあります。wordpressにはプラグインをアクティベートしたときにフックする [register activation hook] がありますが、アップデート時には起動しません。(昔は動いていたらしい。)しばらく前にinit関数に引っ掛けて云々というのを読んだ記憶もあって、面倒だと思いoptionの値を更新しなくてもすむようにやりくりしてきました。

しかしどうしても避けきれなくなり、今回あらためて調べ直したところ、[upgrader process complete] なるすばらしいフックを発見した次第です。プラグインやテーマ、ワードプレスなどのアップグレードにあわせて作動させることができます。どうやら設置されてすでに3年以上経つらしい。今日まで知らんかった・・・。ということで、日本語の記事も見当たらなかったのでメモしておきます。すでに常識なのかもしれませんが。

add_action( 'upgrader_process_complete', 'my_upgrader_process_complete', 10, 2 );

function my_upgrader_process_complete( $upgrader_object, $options ) {
    $current_plugin_path_name = plugin_basename( __FILE__ );
    if ($options['action'] == 'update' && $options['type'] == 'plugin' ){
        foreach($options['plugins'] as $each_plugin){
            if ($each_plugin == $current_plugin_path_name ){

            // YOUR CODES

            }
        }
    }
}

たとえばマルチバイトパッチと自作プラグインの二つを更新したとします。すると$optionsの返り値は、以下のような感じになります。codexではなぜか[packages]になってましたが代わりに[plugins]というkeyが返って来ました。おそらくこの[plugins]は必ず取得できるわけではなく、themeやwpの更新時にはそれぞれのkeyが適宜追加される仕様になっているような気がしますので、必ず[action]と[type]を取得して条件分岐をする必要がありそうです。

Array
(
    [action] => update
    [type] => plugin
    [bulk] => 1
    [plugins] => Array
        (
            [0] => wp-multibyte-patch/wp-multibyte-patch.php
            [1] => my-plugin/my-plugin.php
        )
)

welcart sort product

アーカイブページのループに在庫や価格でのソート・絞り込みを反映させる。

在庫がないのに商品が表示されているのは楽●の空売りみたいで鬱陶しい。商品数が多ければなおのこと。ところが、通常のqueryにwelcartの在庫を 反映させることはできません。一旦ループから離脱して再ループさせれば擬似的に実現できるが、ページ送りやレイアウト崩れの可能性を考えるとテンプレート ごとに細かな対応が必要で面倒臭い。

で、困った時のウェルカスタムさんで探したところ「queryに在庫情報を組み込む現実的な解決法」が掲載されていた。ここによく纏められているように、在庫状態ではなく在庫数で判別しているので縛りがあること、また、複数SKUを持つ商品の場合の処理など、ウェルカートのSKUがシリアライズされているので、価格でのソートなども含めてクエリでの絞り込みの実現は難しそうだ。

ということで、現状はカスタムフィールドにデータを持たせるのが唯一の解決策と思われたので、最終的に以下のようにカスタマイズすることにしました。

各商品記事のカスタムフィールドに必要な情報を書き込む処理

ソートすることを目的にするなら、独立したカスタムフィールドにクエリから参照しやすように数値で登録されていることがベストでしょう。

まずは、以下のようにsave_postにフックしてにソート用のカスタムフィールドを書き込むことにしました。ついでに usces_action_reg_orderdataという受注確定後に処理されるフックがあったので、それにも引っ掛けることにする。 usces_have_skus()はループ内で複数回使うとだめだったりと、扱いが難しいので、get_skusでpostIDからskuのデータ をすべて取得、直接在庫の有無を呼び出す方法を採用、skuを複数展開されていても大丈夫なように、ステータスを一旦配列に入れてから最小の値だけ取り出 すことにします。これで、在庫がなくなったら反映されるという基本要件を満たしてくれます。ちなみに最後のcurrent_screenへのフックはというと、商品マスターの「最新の情報に更新」ボタンを押すとすべての商品データのカスタムフィールドを設定してくれる、という我ながらなんという親切設計!!

/*
* WELCART PRODUCT SORT
* Plugin URI: //web.contempo.jp/weblog/tips/p5438
* Author: Mizuho Ogino 
* Version: 1.0
* License: //www.gnu.org/licenses/gpl.html GPL v2 or later
*/

add_action( 'save_post', 'usces_update_postmeta_for_sorting', 10 );
function usces_update_postmeta_for_sorting( $post_id ){ // 各カスタムフィールドの更新
    if ( !get_post_meta( $post_id, '_itemCode', true ) ) return;
    global $usces;
    $_itemStock = $_itemPrice = array();
    $skus = $usces->get_skus( $post_id );
    if ( $skus ): foreach ( $skus as $sku ): 
        $_itemStock[] = (int)$sku['stock'];
        if ((int)$sku['stock'] == 4 ) continue; // 廃盤を回避
        $_itemPrice[] = (int)$sku['price']; 
    endforeach; endif;
    if ( $_itemStock ) {
        update_post_meta( $post_id, '_itemStock', min( $_itemStock ) ); // 0:在庫あり 1:在庫僅少 2:売り切れ 3:入荷待ち 4:廃盤 の中で一番低いステータス
    }
    if ( $_itemPrice ) {
        update_post_meta( $post_id, '_itemPriceMin', min( $_itemPrice ) ); // SKU中の一番安い設定価格
        update_post_meta( $post_id, '_itemPriceMax', max( $_itemPrice ) ); // SKU中の一番高い設定価格
    }

    global $wpdb;
    $sold = 0;
    $datestr = substr(get_date_from_gmt(gmdate('Y-m-d H:i:s', time())), 0, 10);
    $yearstr = substr($datestr, 0, 4);
    $monthstr = substr($datestr, 5, 2);
    $daystr = substr($datestr, 8, 2);
    $order_table_name = $wpdb->prefix . "usces_order";
    $order_date = date('Y-m-d H:i:s', mktime(0, 0, 0, (int)$monthstr, ((int)$daystr-30), (int)$yearstr)); // 30日分のDBを検索
    $query = "SELECT order_cart FROM {$order_table_name} WHERE order_date >= '{$order_date}'";
    $dbres = $wpdb->get_col($query);
    if( $dbres ): foreach( (array)$dbres as $carts ):
        $rows = unserialize($carts);
        foreach( (array)$rows as $carts ){
            if ( $post_id == $carts['post_id'] ) $sold = $sold + $carts['quantity'];
        }
    endforeach; endif;
    update_post_meta( $post_id, '_itemPopular', $sold ); // 売上数を登録

    return $post_id;
}

add_action( 'usces_action_reg_orderdata', 'usces_action_reg_orderdata_update_postmeta' );
function usces_action_reg_orderdata_update_postmeta( $args ){ // 在庫に変動があればカスタムフィールドを更新
    extract( $args );
    foreach( $cart as $cartrow ){
        $post_id = $cartrow['post_id'];
        usces_update_postmeta_for_sorting( $post_id );
    }
}

add_action( 'current_screen', 'usces_add_actions_to_admin_current_screen' );
function usces_add_actions_to_admin_current_screen( $current_screen ){ // 「最新の情報に更新」ボタンですべてのカスタムフィールドを更新
    if( isset($_GET['page']) && 'usces_itemedit' == $_GET['page'] && isset($_REQUEST['refresh']) ){
        $get_posts = get_posts( array( 'meta_key' => '_itemCode', 'post_type' => 'post', 'numberposts' => -1 ) );
        if( $get_posts ): foreach ( $get_posts as $val ) : 
            usces_update_postmeta_for_sorting( $val->ID );
        endforeach; endif; 
    }
}

welcartにはget_bestseller_idsという関数があったので、当初は「人気順」もダイレクトにベストセラーの順位を登録するように設計してみました。しかしショップサイトの問題点として、「価格」や「在庫」が絶対的な情報であるのに対して、商品の「人気順」が相対的な数値ということがあります。つまり1点売り上げがあるとすべての順位に変動が生じてしまうので、一々すべての商品のカスタムフィールドを更新しなくてはならなくなり、延いてはユーザーサイドの表示速度に影響が出る可能性もあります。wp_cronなどを用いて定期更新にする手も考えましたが、それでも二段階更新になることには変わりないので、人気順の代わりにbestseller_idsも参照している1ヶ月の「売上数」をカウントしてカスタムフィールドに登録することにしました。これならば、売上があった際に該当商品を変更するだけで済みます。(とはいえ、在庫数の変動や記事の更新時にカウントされているので、しばらく売上がない商品は更新されないので完全とは言えない。厳密さを求める向きは手動反映かcronの設置をどうぞ。)

アーカイブページ内での商品一覧のソート処理

ループ全般にフックしてくれるpre_get_postという便利な関数を使用します。$_GET[‘instock’]で在庫の有無を、$_GET[‘sort’]で並び順を変更するという仕様。デフォルトで在庫がないものは通常ループ内で非表示に設定してあります。

つまり、セレクトボックスなどで、カテゴリページのリンクに以下の様に絞り込み用のクエリを設定してやれば、ループが自動的に読み込まれるわけです。例としては以下のようになります。

価格の安い順:
//www.example.com/category/item?sort=price_desc

価格の高い順:
//www.example.com/category/item?sort=price_asc

価格高い順、かつ在庫のない商品もすべて表示:
//www.example.com/category/item?sort=price_asc&instock=false

 

add_action( 'pre_get_posts', 'usces_custom_sort_item', 10, 1 );
function usces_custom_sort_item( $query ) {
    if ( is_admin() || !$query->is_main_query() ) return;
    if ( $query->is_search ) {
        set_query_var('post_type', 'post');
    }
    if ( $query->is_category() || $query->is_tag() ) {
        $args = array();
        $sort = isset($_GET['sort']) && is_string($_GET['sort']) ? $_GET['sort'] : '';
        if ( $sort ){
            if ( $sort == 'price_asc' ) { //価格の安い順 'price_asc'の値は任意に変更可
                $query->set( 'meta_key', '_itemPriceMin' );
                $query->set( 'orderby', array( 'meta_value_num' => 'ASC', 'date' => 'DESC' ) );
            } elseif ( $sort == 'price_desc' ) { //価格の高い順 'price_desc'の値は任意に変更可
                $query->set( 'meta_key', '_itemPriceMax' );
                $query->set( 'orderby', array( 'meta_value_num' => 'DESC', 'date' => 'DESC' ) );
            } elseif ( $sort == 'popular' ) { //人気順(一定期間の売上数の多い順) 'popular'の値は任意に変更可
                $query->set( 'meta_key', '_itemPopular' );
                $query->set( 'orderby', array( 'meta_value_num' => 'DESC', 'date' => 'DESC' ) );
            }
        }
        $instock = isset($_GET['instock']) && is_string($_GET['instock']) ? $_GET['instock'] : '';
        if ( $instock !== 'false' ) { //在庫の有無で抽出 'false'の値は任意に変更可
            $query->set( 
                'meta_query' , array(
                    'relation' => 'OR',
                    array(
                        'key' => '_itemStock',
                        'value' => array( 0, 1 ),
                        'compare' => 'IN',
                    ),
                    array(
                        'key' => '_itemStock',
                        'compare' => 'NOT EXISTS',
                    ),
                    $args
                )
            );
        } elseif ($args ) {
            $query->set( $args );
        }
    }
}

add_filter( 'paginate_links', 'usces_append_query_string', 10 );
add_filter( 'term_link', 'usces_append_query_string', 10 );
function usces_append_query_string( $url ) {// get_term_link // paginate_links のクエリ書き換え
    if ( isset( $_GET['instock'] ) ) $url = add_query_arg( 'instock', $_GET['instock'], $url );
    if ( isset( $_GET['sort'] ) ) $url = add_query_arg( 'sort', $_GET['sort'], $url );
    return $url;
}

言わずもがなですが、在庫がない場合 if ( $instock !== ‘false’ ) のところを if ( $instock === ‘true’ ) とかにしてやれば、trueが設定されている時に「在庫あり」のみを表示(在庫なしは表示しない)というように反転できます。

paginate_linksはその名の通りページネーションやカテゴリのリンク出力にフックしてくれるフィルターで、ページを送ったらソートが解除されると困るので設定してあります。term_linkはカテゴリを選択していってもクエリを継続させます。絞り込み系のサイト構成に向いています。入らなければコメントアウトしましょう。

 

リンクの実際の出力方法など

並べ替えボタンの出力例も書いておきます。

<?php
    $current_url = (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
    $sort = isset($_GET['sort']) && is_string($_GET['sort']) ? $_GET['sort'] : '';
    foreach ( array( 'date'=>'新着順','price_asc'=>'安い順','price_desc'=>'高い順','popular'=>'人気順' ) as $key => $val ):
        $sort_option[ $key ] = "\t\t\t" .'<a class="sort-order'. ( $sort == $key ? ' selected': '' ).'" value="'.$key.'" href="'.( $key == 'date' ? remove_query_arg( 'sort', $current_url ) : add_query_arg( 'sort', $key, $current_url ) ).'" data-target="'.$key.'">' .$val. '</a>'. "\n";
    endforeach;
    $instock = isset($_GET['instock']) && is_string($_GET['instock']) ? $_GET['instock'] : '';
    foreach ( array( 'true'=>'在庫あり','false'=>'すべて表示' ) as $key => $val ):
        $sort_option[ $key ] = "\t\t\t" .'<a class="sort-order'. ( $sort == $key ? ' selected': '' ).'" value="'.$key.'" href="'.( $key == 'date' ? remove_query_arg( 'sort', $current_url ) : add_query_arg( 'sort', $key, $current_url ) ).'" data-target="'.$key.'">' .$val. '</a>'. "\n";
    endforeach;
?>

<select class="sort-group">
    <option value="date" data-url="<?php echo remove_query_arg( 'sort', $current_url ); ?>"<?php echo ( !$sort ? ' selected': '' ); ?>>新着順</option>
    <option value="price_asc" data-url="<?php echo add_query_arg( 'sort', 'price_asc', $current_url ); ?>"<?php echo ( $sort == 'price_asc' ? ' selected': '' ); ?>>価格の安い順</option>
    <option value="price_desc" data-url="<?php echo add_query_arg( 'sort', 'price_desc', $current_url ); ?>"<?php echo ( $sort == 'price_desc' ? ' selected': '' ); ?>>価格の高い順</option>
    <option value="popular" data-url="<?php echo add_query_arg( 'sort', 'popular', $current_url ); ?>"<?php echo ( $sort == 'popular' ? ' selected': '' ); ?>>人気順</option>
</select>
<select class="instock-group">
    <option value="true" data-url="<?php echo remove_query_arg( 'instock', $current_url ); ?>">在庫あり</option>
    <option value="false" data-url="<?php echo add_query_arg( 'instock', 'true', $current_url ); ?>">すべて表示</option>
</select>
<script type="text/javascript" charset="utf-8">
    jQuery( 'select.sort-group, select.instock-group' ).change(function() {
        window.location = jQuery( this ).find( 'option:selected' ).data( 'url' );
    });
</script>

PDF image generatorという拙作プラグインをwordpress.orgに公開しているけれど、そこのフォーラムに「フロントエンドからの投稿では添付ファイルの親投稿が指定されないから、プラグインをカスタマイズする方法を希望うんたら」という、質問があった。プラグインとは関係がなさそうだったけども、つい流れで答えてしまったのでコードだけここにメモしておきます。私自身はなかなか会員制サイトとか作る機会はなさそうですが、万が一役に立つかもしれないしね。

たとえばこんな感じで投稿欄をページテンプレートに設置されている場合の話。

<style type="text/css">
    #__wp-uploader-id-0 a.media-button-insert { font-size:0; }/*#__wp-uploader-id-0 == .media-frame*/
    #__wp-uploader-id-0 a.media-button-insert:before { display:block; content:'記事に挿入する'; font-size:15px; }
</style>
<form action="<?php the_permalink();?>" id="<?php echo $post->ID; ?>" method="POST">
<?php

if ( isset( $_POST['editor_id'] ) ) {
    $post_information = array(
        'post_title' => 'test',
        'post_content' => $_POST['editor_id'],
        'post_type' => 'post',
        'post_status' => 'pending'
    );
    wp_insert_post( $post_information );
}

wp_enqueue_media( array( 'post' => $post->ID )); // upload file to submitpage

$editor_id = 'editor_id';
$args = array(
'textarea_rows' => 15,
'quicktags' => false,
'textarea_name' => $editor_id,
'media_buttons' => true,
'drag_drop_upload' => true,
);

wp_editor( '','editor_id', $args );
?>
<button type="submit"><?php _e('Add Post') ?></button>
</form>

ここで大事な箇所は wp_enqueue_media( array( ‘post’ => $post->ID ))というところ。新しく作成される記事にはIDがまだ存在していないので、アップロードしたメディアを投稿フォームの設置されたページのIDに一旦添付します。そして、以下のようにsave_postフックで投稿処理中に、添付先IDを新しい記事に差し替えます。

<?php

function tempo_id_for_frontend( $attachment_id ){ // run after an attachment is added to the DB
    global $post;
    $user_id = $_SERVER["REMOTE_ADDR"]; // get_current_user_id();
    $submit_id = get_page_by_path( 'your_submit_page_slug' )->ID; // set the page slug of the submit page
    $attachment = get_post( $attachment_id );
    if ( $attachment->post_parent == $submit_id && $user_id ){
        $tempo_ids = get_post_meta( $submit_id, '_id_for_frontend', true); // set attachment id in the customfield of the submit page
        if( !$tempo_ids ) $tempo_ids = array();
        $tempo_ids = $tempo_ids += array( $attachment_id => $user_id );
        update_post_meta( $submit_id, '_id_for_frontend', $tempo_ids );
    }
    return $attachment_id;
}
add_filter( 'add_attachment', 'tempo_id_for_frontend', 99, 2 );

function save_post_for_frontend ( $post_id ) {
    if ( !is_admin() ){
        $user_id = $_SERVER["REMOTE_ADDR"]; // wp_get_current_user()->ID;
        $submit_id = get_page_by_path( 'your_submit_page_slug' )->ID; // set the page slug of the submit page
        if( get_post_type( $post_id ) === 'post' && $user_id ){ 
            $tempo_ids = get_post_meta( $submit_id, '_id_for_frontend', true);
            if( $tempo_ids ) : foreach ($tempo_ids as $key => $val ) : // pick attachment ids from the customfield
                if ( $val == $user_id ){
                    $at_post = array();
                    $at_post['ID'] = $key;
                    $at_post['post_parent'] = $post_id;
                    wp_update_post( $at_post );
                }
                unset( $tempo_ids[$key] );
            endforeach; endif;
            if( $tempo_ids ) update_post_meta( $submit_id, '_id_for_frontend', $tempo_ids );
            else delete_post_meta( $submit_id, '_id_for_frontend' );
        }
    }
}
add_action( 'save_post', 'save_post_for_frontend' );

?>

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%;} }

Really Simple CAPTCHAをコメント欄に応用する

WordPressのスパムコメント、スパムメールは結構困り者で、キャプチャ(画像認証)を導入するケースも多いと思われる。ところで、Contact Form 7には公式モジュールとしてReally Simple CAPTCHAがあり、名前のとおり軽量でデザイン性も高くありながら、大抵のブログにとって必要充分な機能をもたらしてくれるという優れもの。一方でコメントスパム防止に導入できるキャプチャプラグインも幾つかあるわけだが、Contact Form 7を使っているなら、デザインの整合性からいってこれが流用できるとプラグイン構成の簡素化もできてうれしいのでやってみた、という記事です。

 

CODE:using Really Simple CAPTCHA for wordpress comments

以下、コメントフォームへの挿入とヴァリデーションを有効化するコードです。functions.phpに記入、my_comment_captcha以下のフォームの記述は、テーマに合わせて変更します。Contact Form 7での設置の仕方は、公式でどうぞ。

<?php
// コメント欄にアンチスパム・キャプチャを追加
// contact-form7用プラグイン Really Simple CAPTCHA をインストール済の場合だけ有効
if ( !is_user_logged_in() && class_exists('ReallySimpleCaptcha') ) :

function my_comment_captcha() {
    $captcha = new ReallySimpleCaptcha();
    // 以下キャプチャのサイズや色をカスタマイズ
    // $captcha->img_size = array( '72', '24' ); //画像サイズ
    // $captcha->fg = array( '0', '0', '0' ); // 文字色
    // $captcha->bg = array( '255', '255', '255' ); // 画像背景色
    $captcha_word = $captcha->generate_random_word();
    $captcha_prefix = mt_rand();
    $captcha_src = $captcha->generate_image($captcha_prefix, $captcha_word); // Generate CAPTCHA image
?>
<p class="comment-form-captcha">
    <label for="captcha_code">キャプチャ *</label>
    <img src="<?php echo site_url(). '/wp-content/plugins/really-simple-captcha/tmp/' . $captcha_src; ?>" alt="captcha" width="<?php echo $captcha->img_size[0]; ?>" height="<?php echo $captcha->img_size[1]; ?>" />
    <input id="comment_captcha_code" name="comment_captcha_code" size="<?php echo $captcha->char_length; ?>" type="text" />
    <input id="comment_captcha_prefix" name="comment_captcha_prefix" type="hidden" value="<?php echo $captcha_prefix; ?>" />
</p>
<?php
}
add_action( 'comment_form_after_fields' , 'my_comment_captcha' );//#url.inputと#comment.textareaの間に挿入

function my_check_comment_captcha( $approved, $comment_data ) {
    $captcha = new ReallySimpleCaptcha();
    $captcha_prefix = $_POST['comment_captcha_prefix']; // ユーザー入力によるキャプチャレスポンス
    $captcha_code = $_POST['comment_captcha_code']; // ヴァリデーション
    $captcha_correct = $captcha->check( $captcha_prefix, $captcha_code ); // ヴァリデーションチェックが通ると[true]を返す
    if ( !$captcha_correct ) wp_die('キャプチャコードが間違っています。再入力してください。', 200); //エラーコード200を設定すると、他のコメントヴァリデートと同じ挙動になる
    $captcha->remove( $captcha_prefix );
    $captcha->cleanup();
    return $approved;
}
add_filter( 'pre_comment_approved', 'my_check_comment_captcha', 99, 2 );

endif; // if ( class_exists('ReallySimpleCaptcha') ):
?>

 

関連リンクとダウンロード

sc 2015-03-28 14.45.32class="wp-image-3585" src="//web.contempo.jp/wp-content/uploads/2015/03/sc-2015-03-28-14.45.32.png" alt="sc 2015-03-28 14.45.32" width="760" height="361" srcset="https://web.contempo.jp/wp-content/uploads/2015/03/sc-2015-03-28-14.45.32.png 760w, https://web.contempo.jp/wp-content/uploads/2015/03/sc-2015-03-28-14.45.32-640x304.png 640w" sizes="(max-width: 760px) 100vw, 760px" /

実はこれ結構古いネタで、アンチョコは2010年の「Using Really Simple CAPTCHA Plugin for Comments」というやつです。いつの間にかこの記事の筆者はプラグイン化したみたいで、それは試してません。ちょっと弄れば短くなるからfunctions.phpでもええんちゃうかなー、どうせコメント周りの関数いくつかあるしなー、ってことで今にいたります。ヴァリデーション前後の動作も改変してるのでほとんど別ものですが。コンタクトフォームは数あれど、やっぱContact Form 7が好きだなー、という私のような人にどうぞ。

本家サイト:Really Simple CAPTCHA / contact form 7

welcartネタで細かいカスタマイズをひとつ。

welcartの配送希望日の表示がずらっと数字の羅列でちょっと見にくい。曜日とか欲しい。ついでに土日、平日指定なんかも欲しい。

sc-2015-03-05-22.34

 

/////////////////////// 配送希望日のカスタマイズ /////////////////////// 

add_filter( 'usces_delivery_after_days_script', 'my_usces_delivery_after_days_script' );
function my_usces_delivery_after_days_script( $delivery_after_days_script ){
return $delivery_after_days_script = "
 option += '<option value=\"".__('No preference', 'usces')."\">指定しない(一番早い営業日に送付)</option>';
 option += '<option style=\"background-color: #FFF0F5;\" value=\"土日\">土日</option>';
 option += '<option style=\"background-color: #F0FFF1;\" value=\"平日\">平日</option>';
 for(var i = 0; i < delivery_after_days; i++) {
 var newdate = new Date( date[\"year\"]+\"/\"+date[\"month\"]+\"/\"+date[\"day\"] ),
 getday = newdate.getDay(),
 daystyle = '',
 weekday = [\"日\",\"月\",\"火\",\"水\",\"木\",\"金\",\"土\"];
 date_str = date[\"year\"]+\"年\"+parseInt(date[\"month\"])+\"月\"+parseInt(date[\"day\"])+\"日(\"+weekday[getday]+\")\";
 if ( getday == 0 ){
 daystyle = 'background-color: #FFF0F5;'; // 日曜日
 } else if ( getday == 6 ) {
 daystyle = 'background-color: #F0F8FF;'; // 土曜日
 }
 if(date_str == selected_delivery_date) {
 option += '<option style=\"' + daystyle + '\" value=\"' + date_str + '\" selected>' + date_str + '</option>';
 } else {
 option += '<option style=\"' + daystyle + '\" value=\"' + date_str + '\">' + date_str + '</option>';
 }
 date = addDate(date[\"year\"], date[\"month\"], date[\"day\"], 1);
 }";
}

add_filter( 'usces_filter_order_edit_delivery_days_select', 'my_usces_filter_order_edit_delivery_days_select', 10, 3 );
function my_usces_filter_order_edit_delivery_days_select( $delivery_days_select, $data, $delivery_after_days ){
 $delivery_days_select = '<option value="'.__('Non-request', 'usces').'">指定しない(一番早い営業日に送付)</option>';
 $value = '土日';
 $selected = (isset($data['order_delivery_date']) && $data['order_delivery_date'] == $value) ? ' selected="selected"' : '';
 $delivery_days_select .= '<option value="'.$value.'"'.$selected.'>'.$value.'</option>';
 $value = '平日';
 $selected = (isset($data['order_delivery_date']) && $data['order_delivery_date'] == $value) ? ' selected="selected"' : '';
 $delivery_days_select .= '<option value="'.$value.'"'.$selected.'>'.$value.'</option>';
 $data_order_date = explode(" ", $data['order_date']);
 $order_date = explode("-", $data_order_date[0]);
 for($i = 0; $i < $delivery_after_days; $i++) {
 $timestamp = mktime(0,0,0,$order_date[1],$order_date[2]+$i,$order_date[0]);
 $weekday = array( "日", "月", "火", "水", "木", "金", "土" );
 $value = date( 'Y年n月j日', $timestamp ).'('.$weekday[date( 'w', $timestamp )].')';
 $selected = (isset($data['order_delivery_date']) && $data['order_delivery_date'] == $value) ? ' selected="selected"' : '';
 $delivery_days_select .= '<option value="'.$value.'"'.$selected.'>'.$value.'</option>';
 }
 return $delivery_days_select;
}

以上をfunctions.phpに記載して下さい。「usces_delivery_after_days_script」フックはカート画面の表記を変更します。「usces_filter_order_edit_delivery_days_select」フックは受注リスト内の配送希望日セレクタを変更しています。こういう細部のフックもきっちと用意されている当たり、welcartはカスタマイズすることを考えて設計されているんだな、と関心させられますね。

sc-2015-01-25-13.23.51class="wp-image-2977" src="//web.contempo.jp/wp-content/uploads/2015/01/sc-2015-01-25-13.23.51.jpg" alt="sc-2015-01-25-13.23.51" width="800" height="270" /

投稿画面のカテゴリやタグのメタボックスを使いやすくするあれこれ

先日、wordpressの管理画面でのタグクラウドをデフォルトで表示する方法を書きましたが、同じくカテゴリーやカスタムタクソノミノーの表示をあれこれ変更する方法を書いておきます。CMSで納品するときなどには大事な心遣いだと思います。カスタムタクソノミに適用するときは、「category」となっているところをすべてタクソノミ名に変換します。

 

「よく使うもの」リンクとタブを消す。

よく使う項目を表示とか余計なお世話、というときは消してしまう。

div#category-pop, ul#category-tabs { display:none; }

「+ 新規カテゴリーを追加」リンクを消す

同じく、カテゴリを追加ボタンも投稿画面から触ってほしくないときは消してしまう。

div#category-adder { display:none; }

カテゴリメタボックスのmax-heightを解除

デフォルトのカテゴリメタボックスが高さが200pxを超えるとスクロール表示される。視認性を確保したいときはこの設定を消してしまう。

div#category-all { max-height:none; }

カテゴリのチェックボックスをラジオボタンに変換

var checklist = $( '#categorychecklist' );
checklist.each( function(){
    $( this ).find( 'input' ).attr( 'type', 'radio' );
    if ( $( this ).find( 'input' ).is(':checked') === false ) $( this ).children( 'li:first' ).find( 'input' ).prop('checked', true);
});

カテゴリをひとつしか選んでほしくないときは、jQueryでラジオボタンに変換します。

カテゴリのチェックボックスをセレクトボックスに変換

カテゴリの項目が多いときは、jQueryでセレクトボックスに変換して省スペース化を計るととても使いやすいです。

var options=[]; 
$( "#categorychecklist label" ).each(function(){ 
    options.push( '<option value="' + $(this).children("input").val()+ '">' + $(this).html().replace(/<input[^>]+>/g, '') + '</option>' ); 
});
$( "#categorychecklist" ).replaceWith( '<select id="categorychecklist" name="tax_input[category][]" style="width:100%; margin:10px 0; ">' + options.join( '' ) + '</select>' );

チェックボックスを並び替えないようにする

ついでに並び順のデフォルトが、チェックが入っているボックスが一番上にくるようになっていますので、それも外してみます。これはフックで。

add_filter( 'wp_terms_checklist_args', 'stop_checked_ontop' ,10,2); 
function stop_checked_ontop( $args, $post_id ){ 
    $args['checked_ontop'] = false; 
    return $args; //チェックボックスの並び替え停止
}

 

CODE : Hook to admin_head

タグクラウドのデフォルト表示もまとめたコードです。以下の関数をお好みでカスタマイズしてadmin_headフックしたものをfunctions.phpに記載します。当然ですが、このまま有効にすると、ラジオボタンに変換する関数とセレクトボックスに変換する関数とがコンフリクトします。

<?php
function my_admin_head(){ 
?>
<style type="text/css">
    /*  カテゴリ欄のタブ非表示  */ 
    /*  remove tabs from category metabox  */ 
    div#category-pop, ul#category-tabs { display:none; }

    /*  カテゴリ追加ボタン非表示  */ 
    /*  remove category add button from category metabox  */
    div#category-adder { display:none; }

    /*  カテゴリ欄を最大サイズに変更  */ 
    /*  unset max-height of category metabox  */
    div#category-all { max-height:none; }
</style>
<script type="text/javascript">
jQuery(function( $ ){ 

    /*  チェックボックスをラジオボタンに変換  */
    /*  change category check box into radio button  */
    var checklist = $( '#categorychecklist' );
    checklist.each( function(){
        $( this ).find( 'input' ).attr( 'type', 'radio' );
        if ( $( this ).find( 'input' ).is(':checked') === false ) $( this ).children( 'li:first' ).find( 'input' ).prop('checked', true);
    });

    /*  チェックボックスをセレクトボックスに変換  */
    /*  change category check box into select box  */
    var options=[]; 
    $( "#categorychecklist label" ).each(function(){ 
        options.push( '<option value="' + $(this).children("input").val()+ '">' + $(this).html().replace(/<input[^>]+>/g, '') + '</option>' ); 
    });
    $( "#categorychecklist" ).replaceWith( '<select id="categorychecklist" name="tax_input[category][]" style="width:100%; margin:10px 0; ">' + options.join( '' ) + '</select>' );

    /*  タグクラウドをデフォルトで表示に変更  */
    /*  automatically display the popular tags  */
    $( 'a.tagcloud-link' ).each( function(){ 
        tagBox.get( $(this).attr( 'id' ) ); 
        $(this).html('').unbind().click(function(){ 
            $(this).siblings( '.the-tagcloud' ).toggle(); return false; 
        }); 
        return false; 
    });
});
</script>
<?php 
}
add_filter( 'admin_head', 'my_admin_head' );

function stop_checked_ontop( $args, $post_id ){ 
    if ( $args['checked_ontop'] !== false ) $args['checked_ontop'] = false; 
    return $args; //チェックボックスの並び替え停止
} 
add_filter( 'wp_terms_checklist_args', 'stop_checked_ontop' ,10,2); 
?>

参考

ラジオボタンとチェックボックスのソート解除は以下を参考にしました。あとは見当たらなかったので自作。

Category checkbox list tree changes when editing a post

WordPressのカスタムタクソノミーのチェックボックスをラジオボタンにする方法(クイック編集でもチェックボックスの制限をしたい場合なども参考になります。)