WordPress 限制 Category 只有兩層,並搭配 ACF 實作只能屬於一種主子分類

我們都知道 WordPress 的 Category 理論上可以有無限多層,並且一個 Post 可以屬於多個不同的 Category (跨多個主子分類),這在某些應用情境裡會造成很多困擾。

我們今天要實作的情境是:

  1. 只能有兩層分類
  2. Post 只能有一個父層分類和只能有一個子層分類

限制兩層分類

我們可以使用 taxonomy_parent_dropdown_args 這個來限制下拉選單中可設定的父層分類僅有第一層

add_filter('taxonomy_parent_dropdown_args', function($args, $taxonomy, $context) {
    if ($taxonomy == 'category') {
        $args['depth'] = 1;
    }
    return $args;
}, 10, 3);

Post 只能有一個父層分類和只能有一個子層分類

  1. 使用 ACF 開設分類法,並且將 metabox 關掉。
  2. 使用 ACF 開設兩個欄位,分別為 main_category 和 sub_category 其類型為 select
  3. 使用 acf/load_field filter 來產生選項
function acf_load_main_category_field_choices( $field ) {
    $field['choices'] = array();

    $terms = get_terms([
        'taxonomy' => 'category',
        'parent' => 0,
        'hide_empty' => false,
    ]);

    $field['choices'][ 0 ] = '請選擇';

    foreach ($terms as $term) {
        $field['choices'][ $term->term_id ] = $term->name;
    }

    return $field;
}
add_filter('acf/load_field/key=field_658d1a664afbb', 'acf_load_main_category_field_choices');
function acf_load_sub_category_field_choices( $field ) {

    $field['choices'] = array();

    $terms = array_filter(get_terms([
        'taxonomy' => 'category',
        'hide_empty' => false,
    ]), function($term) {
        return $term->parent != 0;
    });

    $field['choices'][ 0 ] = '請選擇';

    foreach ($terms as $term) {
        $field['choices'][ $term->term_id ] = $term->name;
    }

    return $field;
}

add_filter('acf/load_field/key=field_658d1a8f4afbc', 'acf_load_sub_category_field_choices');
  1. 建立 category 樹狀結構
function digitspark_get_taxonomy_array($taxonomy) {
    $terms = get_terms([
        'taxonomy' => 'user-guide-category',
        'hide_empty' => false,
    ]);

    return digitspark_get_taxonomy_tree($terms, 0);
}

function digitspark_get_taxonomy_tree($nodes, $parentId) {
    $tree = [];

    if (is_array($nodes)) {
        $roots = array_filter($nodes, function($node) use ($parentId) {
            return $node->parent == $parentId;
        });

        $index = 0;

        foreach ($roots as $rnode) {
            $tree[$index++] = [
                'node' => $rnode,
                'children' => digitspark_get_taxonomy_tree($nodes, $rnode->term_id)
            ];
        }   
    }

    return $tree;
}
  1. 使用 4 的樹狀搭配 JavaScript 來控制前端選項
function acf_load_categories_js() {
    $items = digitspark_get_taxonomy_array('category');
?>
<script type="text/javascript">
(function($) {
 var categories_tree = <?php echo json_encode($items); ?>;
    function updateSubCategories() {
        // 當主分類被選的時候,更新子分類的選項
        initSelection()
        // 如果確定父分類有被選,則要顯示子分類的選項,並修改子分類為空
        const parent = parseInt(window.jQuery('#acf-field_658d1a664afbb').val())
        if (parent) {
            const key = '#acf-field_658d1a8f4afbc option[value=0]'
            window.jQuery(key).show()
            window.jQuery('#acf-field_658d1a8f4afbc').show()
            window.jQuery('#acf-field_658d1a8f4afbc').val(0)
        }
    }

    window.jQuery('#acf-field_658d1a664afbb').change(updateSubCategories)

    function initSelection() {
        const parent = parseInt(window.jQuery('#acf-field_658d1a664afbb').val())
        if (parent) {
            // find children in tree
            const node = categories_tree.filter(function(item) {
                return item.node.term_id == parent
            })
            // hide those options
            window.jQuery('#acf-field_658d1a8f4afbc option').hide()
            node[0].children.forEach(function(item) {
                const key = '#acf-field_658d1a8f4afbc option[value="' + item.node.term_id + '"]'
                window.jQuery(key).show()
            })
        } else {
            window.jQuery('#acf-field_658d1a8f4afbc').hide()
        }
    }

})(jQuery); 
</script>
<?php
}

add_action('acf/input/admin_footer', 'acf_load_user_guide_categories_js');