Własna kontrolka wyboru kategorii

Ostatnio opisałem tworzenie prostej kontrolki kalendarza, a w tym wpisie chciałbym opisać trochę bardziej złożoną kontrolkę służącą do tworzenia list kategorii podlegającym określonym warunkom w naszym motywie.

Taka kontrolka idealnie sprawdzi się gdy chcemy np. zastosować inny układ strony do określonych kategorii naszego bloga. Dodatkowo dowiecie się jak tworzyć kontrolki, które korzystają z wielu pól formularza w obrębie jednej kontrolki.

Wiele pól w jednej kontrolce

Podstawowym problemem jest sposób przechowywania danych. Nasza kontrolka będzie posiadać czasem kilka a czasem może i kilkadziesiąt pól typu checkbox (zależnie od liczby kategorii na stronie użytkownika). Oczywiście nie ma sensu generować każdego tego typu pola jako osobnej kontrolki. Dużo wygodniej będzie umieścić informację o wybranych polach w jednym ukrytym polu typu hidden.

Idea działania

Nasza kontrolka musi działać następująco – w momencie zaznaczenia danego pola typu checkbox w ukrytym polu musi wstawiać się ID wybranej kategorii – te ID będą rozdzielone przecinkami co pozwoli nam potem łatwo operować na wartości ukrytego pola (przy usuwaniu wyboru kategorii) oraz przetwarzać dane opcji w samym WordPressie z poziomu kodu PHP.

Kod kontrolki

Zacznijmy od bazowej struktury klasy naszej kontrolki:

class Dziudek_Category_Checklist extends WP_Customize_Control {
    public $type = 'dziudek_category_checklist';

    public function __construct($manager, $id, $args = array()) {
        parent::__construct($manager, $id, $args);
    }

    public function enqueue() {
        wp_enqueue_script('dziudek-category-checklist', get_template_directory_uri() . '/inc/dziudek-category-checklist/dziudek-category-checklist.js' );
        wp_enqueue_style('dziudek-category-checklist', get_template_directory_uri() . '/inc/dziudek-category-checklist/dziudek-category-checklist.css');
    }

    public function render_content() {
        ?>
        <label>
            <?php if(!empty($this->label)) : ?>
            <span class="customize-control-title">
                <?php echo esc_html( $this->label ); ?>
            </span>
            <?php endif; ?>

            <?php if(!empty($this->description)) : ?>
            <span class="description customize-control-description">
                <?php echo $this->description ; ?>
            </span>
            <?php endif; ?>

            <!— Miejsce na właściwy kod kontrolki —>
        </label>
        <?php
    }
}

Jest to standardowy kod, który już wcześniej opisywałem. Co ważne – kod tworzyłem pod strukturę szablonu Twenty Fourteen – stąd pliki JS i CSS kontrolki znajdują się w katalogu inc.

Zaś sam kod generujący pola typu checkbox wygląda następująco:

<div class="dziudek-category-checklist">
    <ul>
        <?php wp_category_checklist(0, 0, explode(',', $this->value()), false, new Dziudek_TC_Walker_Category_Checklist($this->id), false); ?>
    </ul>

    <input type="hidden" id="<?php echo $this->id; ?>" class="dziudek-category-checklist-value" <?php $this->link(); ?> value="<?php echo sanitize_text_field( $this->value() ); ?>">
</div>

W powyższym kodzie sporo się dzieje więc po kolei:

  • listę checkboksów generujemy gotową funkcją – wp_category_checklist (można też wygenerować dowolną listę taksonomii z użyciem wp_terms_checklist), która jako trzeci argument przyjmuje listę ID wybranych kategorii rozdzielonych przecinkiem (przypadek? ;)),
  • w piątym argumencie nadpisujemy funkcję, która generuje elementy listy własną klasą, którą opiszę za chwilę,
  • na koniec tworzymy ukryte pole, które przechowa wartość kontrolki (dla celów testowych można mu tymczasowo zmienić typ na text) – oczywiście żeby ekran personalizacji reagował na zmiany wartości opcji od razu wywołujemy też metodę $this->link().

Pora na klasę generującą elementy listy:

class Dziudek_TC_Walker_Category_Checklist extends Walker {
    var $db_fields = array ('parent' => 'parent', 'id' => 'term_id');
    var $field_name = '';

    function __construct($field_name) {
        $this->field_name = $field_name;
    }

    function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat("\t", $depth);
        $output .= "$indent<ul class='children'>\n";
    }

    function end_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat("\t", $depth);
        $output .= "$indent</ul>\n";
    }

    function start_el( &$output, $category, $depth = 0, $args = array(), $id = 0 ) {
        extract($args);
        $output .= "\n<li>" . '<label><input value="' . $category->term_id . '" type="checkbox" ' . checked(in_array($category->term_id, $selected_cats), true, false ) . disabled(empty($args['disabled']), false, false) . ' class="gk-category-checklist-checkbox" data-id="'.$this->field_name.'" data-category-id="'.$category->term_id.'" /> ' . esc_html( apply_filters('the_category', $category->name )) . '</label>';
    }

    function end_el( &$output, $category, $depth = 0, $args = array() ) {
        $output .= "</li>\n";
    }
}

W powyższym kodzie najważniejsze są dwie rzeczy:

  • pole $db_fields, które pozwala określić pola obiektów taksonomii, które będą wykorzystane do generowania struktury listy,
  • metoda start_el, która odpowiada za interesującą nas strukturę elementu listy

Kilka ważnych elementów metody start_el:

value="'.$category->term_id.'"

Powyższy fragment odpowiada za ustawienie właściwej wartości pola.

checked(in_array($category->term_id, $selected_cats), true, false)

W tym fragmencie generowane jest zaznaczenie właściwych pól na podstawie wyboru użytkownika przy generowaniu struktury kontrolki.

data-id="'.$this->field_name.'" data-category-id="'.$category->term_id.'"

Na potrzeby późniejszych skryptów dodałem powyższe atrybuty dla łatwiejszej obsługi zmian wartości pól.

Kod CSS dla naszej kontrolki nie jest zbyt rozbudowany:

.dziudek-category-checklist {
    min-height: 42px;
    max-height: 200px;
    overflow: auto;
    padding: .9em;
    border: 1px solid #dfdfdf;
    background-color: #fdfdfd;
}
.dziudek-category-checklist .children {
    padding: 5px 0;
}
.dziudek-category-checklist .children li {
    padding-left: 20px;
}

Najważniejsza tutaj jest właściwość max-height – jest to bardzo istotne, ponieważ niektórzy mogą mieć bardzo dużo kategorii, zatem zabezpiecza nas to przed bardzo długą listą wyboru ingerującą w układ ekranu personalizacji.

Kod JS dla naszej kontrolki jest trochę bardziej rozbudowany. Zacznijmy od dodania obsługi zdarzeń dla pól typu checkbox:

$(window).load(function() {
    $('.gk-category-checklist-checkbox').each(function(i, checkbox) {
        checkbox = $(checkbox);

        checkbox.on('change', function(e) {
            e.stopPropagation();
            var id = $(this).attr('data-id');
            var category_id = $(this).attr('data-category-id');

            if(checkbox.prop('checked') == true ) {
                add_checked_category(category_id, id);
            } else {
                remove_checked_category(category_id, id);
            }
        });
    });
});

W powyższym kodzie każda kontrolka typu checkbox ma dodawaną obsługę zdarzenia change, które występuje przy zmianie wartości kontrolki. Pobierane są z kontrolki dwie informacje – nazwa listy z którą powiązana jest dane pole oraz ID kategorii, która ma zostać dodana lub usunięta. Na podstawie stanu wyboru kontrolki ID kategorii jest dodawane lub usuwane do listy w ukrytym polu.

Dodawanie ID kategorii wykonuje poniższa funkcja:

function add_checked_category(category, control) {
    var value = wp.customize.instance(control).get().split(',');
    value = value.filter(Number);

    if(value.indexOf(category) === -1) {
        value.push(category);
        wp.customize.instance(control).set(value.join());
    }
}

Natomiast usuwanie wykonywane jest przez funkcję o następującej postaci:

function remove_checked_category(category, control) {
    var value = wp.customize.instance(control).get();
    value = value.split(',');
    var category_index = value.indexOf(category);

    if(category_index >= 0) {
        value.splice(category_index, 1);
        value = value.join();
        wp.customize.instance(control).set( value);
    }
}

Jak widać obie funkcje przyjmują dwa argumenty – ID kategorii oraz identyfikator kontrolki (ukrytego pola przechowującego listę ID wybranych kategorii).

Co ważne nie modyfikujemy wartości samego ukrytego pola bezpośrednio ale poprzez API ekranu personalizacji i metodę wp.customize.instance(), która automatycznie zmienia i pobiera wartość ukrytego pola.

Wykorzystujemy naszą kontrolkę

Aby nasza kontrolka pojawiła się w ekranie personalizacji, wywołujemy ją w następujący sposób:

$wp_customize->add_setting('blog_categories', array(
	'default' => '',
        'sanitize_callback' => ‘dziudek_validate_category_checklist'
    ));

    $wp_customize->add_control(new Dziudek_Category_Checklist(
    $wp_customize,
    'blog_categories',
    array(
	'label' => 'Select categories',
        'section' => 'featured_content'
    )
));

Funkcja sprawdzająca wartość opcji wygląda następująco:

function dziudek_validate_category_checklist($input) {
    $temp = explode(',', $input);

    if(count($temp) === count(array_filter($temp, 'is_numeric'))) {
        return $input;
    }

    return null;
}

Nasza kontrolka powinna się prezentować mniej więcej tak:

theme-customizer-custom-control-category-selection

Gdy wydaje się, że wszystko już działa…

… odkrywamy mały problem. Jeżeli korzystamy z przeglądarki Firefox, to zobaczymy, że wybierając kategorie coś dziwnego dzieje się z pierwszym checkboxem – zaznacza i odznacza się on przy wyborze innych kategorii.

Niestety do tej pory nie znalazłem dokładnej przyczyny tego problemu. Jedyne co wiem to to, że to najprawdopodobniej problem ze skryptami ekranu personalizacji, bo na stronie bez kodu JS takiego zachowania nie zaobserwowałem oraz problem występuje nawet bez dodania kodu JS kontrolki.

Na szczęście znalazłem łatwe obejście problemu – ponieważ zawsze zaznacza/odznacza się pierwsze pole typu checkbox, wystarczy stworzyć na początku listy dodatkowe ukryte pole tego typu:

<li class="dziudek-fake-checkbox"><input type="checkbox" /></li>

Oraz oczywiście dodać odpowiedni kod CSS:

.dziudek-fake-checkbox {
    display: none;
}

Co rozwiązuje problem. Jeżeli znajdę przyczynę tego problemu to z pewnością opiszę ją tutaj na blogu.

Co dalej?

Powyższy kod kontrolki można łatwo dostosować do innych potrzeb – możemy na przykład stworzyć listę, która będzie pozwalała dla każdej kategorii określić inny układ – zamieniając pola typu checkbox na pola select z listą układów. W takiej sytuacji dane przechowywalibyśmyw formacie ID1=layout,ID2=layout,ID3=layout...

Podsumowanie

Polecam własne eksperymenty w tym temacie, gdyż dzięki opisywanemu sposobowi wykorzystania ukrytego pola przechowującego wartości z innych pól, można stworzyć bardzo ciekawe kontrolki i znacząco poszerzyć możliwości naszego motywu oraz zwiększyć wygodę użytkownika.

Aktualizacja

18.XII.2014: Po zainstalowaniu najnowszego WordPressa odkryłem, że nie działają zdarzenia kontrolek – pomogła zmiana zdarzenia DOMContentLoaded na onLoad.