8889841creadme.md000064400000007061150513302770006332 0ustar00# CMB2 Field Type: Select2 ## Description [Select2](https://select2.github.io/) field type for [CMB2](https://github.com/WebDevStudios/CMB2 "Custom Metaboxes and Fields for WordPress 2"). This plugin gives you two additional field types based on Select2: 1. The `pw_taxonomy_select` field acts much like the default `select` field. However, it adds typeahead-style search allowing you to quickly make a selection from a large list 2. The `pw_taxonomy_multiselect` field allows you to select multiple values with typeahead-style search. The values can be dragged and dropped to reorder ## Installation You can install this field type as you would a WordPress plugin: 1. Download the plugin 2. Place the plugin folder in your `/wp-content/plugins/` directory 3. Activate the plugin in the Plugin dashboard Alternatively, you can include this field type within your plugin/theme. The path to front end assets (JS/CSS) can be filtered using `pw_cmb2_field_select2_asset_path`. See an example where we [load assets from the current active theme](http://link.from.pw/pw_cmb2_field_select2_asset_path). ## Usage `pw_taxonomy_select` - Select box with with typeahead-style search. Example: ```php $cmb->add_field( array( 'name' => 'Cooking time', 'id' => $prefix . 'cooking_time', 'desc' => 'Cooking time', 'type' => 'pw_taxonomy_select', 'taxonomy' => 'category', ) ); ``` `pw_taxonomy_multiselect` - Multi-value select box with drag and drop reordering. Example: ```php $cmb->add_field( array( 'name' => 'Ingredients', 'id' => $prefix . 'ingredients', 'desc' => 'Select ingredients. Drag to reorder.', 'type' => 'pw_taxonomy_multiselect', 'taxonomy' => 'category', ) ); ``` ### Placeholder You can specify placeholder text through the attributes array. Example: ```php $cmb->add_field( array( 'name' => 'Ingredients', 'id' => $prefix . 'ingredients', 'desc' => 'Select this recipes ingredients.', 'type' => 'pw_taxonomy_multiselect', 'options' => array( 'flour' => 'Flour', 'salt' => 'Salt', 'eggs' => 'Eggs', 'milk' => 'Milk', 'butter' => 'Butter', ), 'attributes' => array( 'placeholder' => 'Select ingredients. Drag to reorder' ), ) ); ``` ### Custom Select2 configuration and overriding default configuration options You can define Select2 configuration options using HTML5 `data-*` attributes. It's worth reading up on the [available options](https://select2.github.io/options.html#data-attributes) over on the Select2 website. Example: ```php $cmb->add_field( array( 'name' => 'Ingredients', 'id' => $prefix . 'ingredients', 'desc' => 'Select ingredients. Drag to reorder.', 'type' => 'pw_taxonomy_multiselect', 'options' => array( 'flour' => 'Flour', 'salt' => 'Salt', 'eggs' => 'Eggs', 'milk' => 'Milk', 'butter' => 'Butter', ), 'attributes' => array( 'data-maximum-selection-length' => '2', ), ) ); ``` ## Helper functions You may want to populate the options array dynamically. Common use cases include listing out posts and taxonomy terms. I've written a number of generic helper functions which can be used to return a CMB2 style array for both [posts](http://link.from.pw/1PkJmWc) and [terms](http://link.from.pw/1TDArjR). ## Limitations/known issues If you’d like to help out, pull requests are more than welcome! * This field does not work well as a repeatable field within a repeatable group. * Yoast SEO also loads Select2. Currently a version behind, there is an issue with the previous version of Select2 and it's ability to position the dropdown relative to the field. cmb-field-taxonomy-select2-search.php000064400000025227150513302770013570 0ustar00setup_admin_scripts(); if ( version_compare( CMB2_VERSION, '2.2.2', '>=' ) ) { $field_type_object->type = new CMB2_Type_Select( $field_type_object ); } echo $field_type_object->select( array( 'class' => 'pw_taxonomy_select2_search pw_taxonomy_select_search', 'desc' => $field_type_object->_desc( true ), 'options' => $this->get_pw_taxonomy_options( $field_escaped_value, $field_type_object ), 'data-placeholder' => $field->args( 'attributes', 'placeholder' ) ? $field->args( 'attributes', 'placeholder' ) : $field->args( 'description' ), 'data-taxonomy' => $field_type_object->field->args( 'taxonomy' ), ) ); } /** * Render multi-value select input field */ public function render_pw_taxonomy_multiselect( $field, $field_escaped_value, $field_object_id, $field_object_type, $field_type_object ) { $this->setup_admin_scripts(); if ( version_compare( CMB2_VERSION, '2.2.2', '>=' ) ) { $field_type_object->type = new CMB2_Type_Select( $field_type_object ); } $a = $field_type_object->parse_args( 'pw_taxonomy_multiselect_search', array( 'multiple' => 'multiple', 'style' => 'width: 99%', 'class' => 'pw_taxonomy_select2_search pw_taxonomy_multiselect_search', 'name' => $field_type_object->_name() . '[]', 'id' => $field_type_object->_id(), 'desc' => $field_type_object->_desc( true ), 'options' => $this->get_pw_taxonomy_options( $field_escaped_value, $field_type_object ), 'data-placeholder' => $field->args( 'attributes', 'placeholder' ) ? $field->args( 'attributes', 'placeholder' ) : $field->args( 'description' ), 'data-taxonomy' => $field_type_object->field->args( 'taxonomy' ), ) ); $attrs = $field_type_object->concat_attrs( $a, array( 'desc', 'options' ) ); echo sprintf( '%s%s', $attrs, $a['options'], $a['desc'] ); } /** * Return list of options for pw_taxonomy_multiselect * * Return the list of options, with selected options at the top preserving their order. This also handles the * removal of selected options which no longer exist in the options array. */ public function get_pw_taxonomy_options( $field_escaped_value, $field_type_object ) { $options = array(); $field_escaped_value = $this->options_terms($field_type_object->field); // If we have selected items, we need to preserve their order if ( ! empty( $field_escaped_value ) ) { if ( !is_array($field_escaped_value) ) { $field_escaped_value = array($field_escaped_value); } $options = (array) $this->get_terms($field_type_object->field->args( 'taxonomy' ), array('include' => $field_escaped_value) ); // $options = $this->sort_array_by_array( $options, $field_escaped_value ); } $selected_items = ''; $other_items = ''; foreach ( $options as $opt ) { // Clone args & modify for just this item $option = array( 'value' => $opt['id'], 'label' => $opt['name'], ); // Split options into those which are selected and the rest if ( in_array( $opt['id'], (array) $field_escaped_value ) ) { $option['checked'] = true; $selected_items .= $field_type_object->select_option( $option ); } else { $other_items .= $field_type_object->select_option( $option ); } } return $selected_items . $other_items; } public function options_terms($field) { if ( empty($field->data_args()['id']) ) { return array(); } $object_id = $field->data_args()['id']; $terms = get_the_terms( $object_id, $field->args( 'taxonomy' ) ); if ( ! empty( $terms ) && ! is_wp_error( $terms ) ){ foreach ( $terms as $index => $term ) { $terms[ $index ] = $term->term_id; } } return $terms; } /** * Sort an array by the keys of another array * * @author Eran Galperin * @link http://link.from.pw/1Waji4l */ public function sort_array_by_array( array $array, array $orderArray ) { $ordered = array(); foreach ( $orderArray as $key ) { if ( array_key_exists( $key, $array ) ) { $ordered[ $key ] = $array[ $key ]; unset( $array[ $key ] ); } } return $ordered + $array; } /** * Handle sanitization for repeatable fields */ public function pw_taxonomy_multiselect_sanitize( $check, $meta_value, $object_id, $field_args ) { if ( empty($meta_value) || !is_array( $meta_value ) ) { return $check; } if ( $field_args['repeatable'] ) { foreach ( $meta_value as $key => $val ) { $meta_value[$key] = array_map( 'absint', $val ); wp_set_object_terms( $object_id, array_map( 'absint', $val ), $field_args['taxonomy'], false ); } } else { $meta_value = array_map( 'absint', $meta_value ); wp_set_object_terms( $object_id, $meta_value, $field_args['taxonomy'], false ); } return $meta_value; } /** * Handle sanitization for repeatable fields */ public function pw_taxonomy_select_sanitize( $check, $meta_value, $object_id, $field_args ) { if ( empty( $meta_value ) ) { return $check; } if ( is_array($meta_value) ) { $meta_value = array_map( 'absint', $meta_value ); } else { $meta_value = intval($meta_value); } wp_set_object_terms( $object_id, $meta_value, $field_args['taxonomy'], false ); return $meta_value; } /** * Handle escaping for repeatable fields */ public function pw_taxonomy_multiselect_escaped_value( $check, $meta_value, $field_args ) { if ( ! is_array( $meta_value ) || ! $field_args['repeatable'] ) { return $check; } foreach ( $meta_value as $key => $val ) { $meta_value[$key] = array_map( 'esc_attr', $val ); } return $meta_value; } /** * Add 'table-layout' class to multi-value select field */ public function pw_taxonomy_multiselect_table_row_class( $check ) { $check[] = 'pw_taxonomy_multiselect_search'; return $check; } /** * Enqueue scripts and styles */ public function setup_admin_scripts() { $asset_path = apply_filters( 'pw_cmb2_field_select2_asset_path', plugins_url( '', __FILE__ ) ); wp_enqueue_script( 'pw-taxonomy-select2-loadmore-init', $asset_path . '/js/script.js', array( 'cmb2-scripts', 'wpjbp-select2', 'jquery-ui-sortable' ), self::VERSION ); wp_enqueue_style( 'pw-taxonomy-select2-loadmore-tweaks', $asset_path . '/css/style.css', array( 'wpjbp-select2' ), self::VERSION ); wp_localize_script( 'pw-taxonomy-select2-loadmore-init', 'wp_job_board_pro_tax_search_opts', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'ajaxurl_endpoint' => WP_Job_Board_Pro_Ajax::get_endpoint(), )); } public function get_terms($taxonomy, $query_args = array(), $per_page = 0, $page = 1) { $return = array(); $offset = ( $page - 1 ) * $per_page; $defaults = array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', 'number' => $per_page, 'offset' => $offset, 'lang' => apply_filters( 'wp-job-board-pro-current-lang', null ) ); $args = wp_parse_args( $query_args, $defaults ); $terms_hash = 'wjbp_cats_' . md5( wp_json_encode( $args ) . WP_Job_Board_Pro_Cache_Helper::get_transient_version('wjbp_get_' . $taxonomy) ); $terms = get_transient( $terms_hash ); if ( empty( $terms ) ) { $terms = get_terms( $taxonomy, $args ); set_transient( $terms_hash, $terms, DAY_IN_SECONDS * 7 ); } if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) { $return = array(); foreach( $terms as $term ) { $return[] = array('id' => $term->term_id, 'name' => $term->name); } } return $return; } public function get_terms_count($taxonomy, $query_args = array() ) { $defaults = array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', 'lang' => apply_filters( 'wp-job-board-pro-current-lang', null ) ); $args = wp_parse_args( $query_args, $defaults ); $terms_hash = 'wjbp_cats_' . md5( wp_json_encode( $args ) . WP_Job_Board_Pro_Cache_Helper::get_transient_version('wjbp_get_' . $taxonomy) ); $total_terms = get_transient( $terms_hash ); if ( empty( $total_terms ) ) { $total_terms = wp_count_terms( $taxonomy, $args ); set_transient( $terms_hash, $total_terms, DAY_IN_SECONDS * 7 ); } return $total_terms; } public function search_terms() { $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : ''; $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 1; $taxonomy = isset($_GET['taxonomy']) ? sanitize_text_field($_GET['taxonomy']) : ''; $parent = isset($_GET['parent']) ? sanitize_text_field($_GET['parent']) : ''; $per_page = 6; $args = array('search' => $search, 'parent' => $parent); $terms = $this->get_terms($taxonomy, $args, $per_page, $page); $options = array(); if ( $terms ){ $options = $terms; } $total_terms = $this->get_terms_count($taxonomy, $args); $pages = ceil($total_terms/$per_page); $return = array( 'pages' => $pages, 'results' => $options, ); wp_send_json($return); } } $pw_cmb2_field_select2 = new PW_CMB2_Field_Taxonomy_Select2_Search(); css/style.css000064400000000426150513302770007213 0ustar00.cmb-type-pw-taxonomy-multiselect-search .select2-selection__choice, .cmb-type-pw-taxonomy-multiselect-search .select2-search--inline { margin-bottom: 0; line-height: inherit; } .cmb-type-pw-taxonomy-multiselect-search .select2-selection__choice { cursor: move !important; }js/script.js000064400000013654150513302770007036 0ustar00(function ($) { 'use strict'; var __cache = []; $('.pw_taxonomy_select_search').each(function () { select_init($(this)); }); function select_init($element) { var allowclear = $element.data('allowclear'); var width = $element.data('width') ? $element.data('width') : '100%'; var $taxonomy = $element.data('taxonomy'); $element.select2({ allowClear: allowclear, width: width, width: '100%', dir: wp_job_board_pro_select2_opts['dir'], language: { noResults: function (params) { return wp_job_board_pro_select2_opts['language_result']; }, inputTooShort: function () { return wp_job_board_pro_select2_opts['formatInputTooShort_text']; } }, ajax: { url: wp_job_board_pro_tax_search_opts.ajaxurl_endpoint.toString().replace( '%%endpoint%%', 'wpjb_search_terms' ), dataType: 'json', delay: 250, data: function (params) { var query = { search: params.term, page: params.page || 1, taxonomy: $taxonomy, } // Query parameters will be ?search=[term]&type=public return query; }, processResults: function (data, params) { params.page = params.page || 1; return { results: $.map(data.results, function (item) { return { text: item.name, id: item.id } }), pagination: { more: params.page < data.pages } }; }, transport: function(params, success, failure) { //retrieve the cached key or default to _ALL_ var __cachekey = params.data.search + '-' + params.data.taxonomy + '-' + params.data.page; if ('undefined' !== typeof __cache[__cachekey]) { //display the cached results success(__cache[__cachekey]); return; /* noop */ } var $request = $.ajax(params); $request.then(function(data) { //store data in cache __cache[__cachekey] = data; //display the results success(__cache[__cachekey]); }); $request.fail(failure); return $request; }, cache: true }, placeholder: 'Search for a repository', minimumInputLength: 2 }); } $.fn.extend({ select2_sortable: function () { var select = $(this); select_init($(this)); var ul = $(select).next('.select2-container').first('ul.select2-selection__rendered'); ul.sortable({ containment: 'parent', items : 'li:not(.select2-search--inline)', tolerance : 'pointer', stop : function () { $($(ul).find('.select2-selection__choice').get().reverse()).each(function () { var id = $(this).data('data').id; var option = select.find('option[value="' + id + '"]')[0]; $(select).prepend(option); }); } }); } }); $('.pw_taxonomy_multiselect_search').each(function () { $(this).select2_sortable(); }); // Before a new group row is added, destroy Select2. We'll reinitialise after the row is added $('.cmb-repeatable-group').on('cmb2_add_group_row_start', function (event, instance) { var $table = $(document.getElementById($(instance).data('selector'))); var $oldRow = $table.find('.cmb-repeatable-grouping').last(); $oldRow.find('.pw_taxonomy_select2_search').each(function () { $(this).select2('destroy'); }); }); // When a new group row is added, clear selection and initialise Select2 $('.cmb-repeatable-group').on('cmb2_add_row', function (event, newRow) { $(newRow).find('.pw_taxonomy_select_search').each(function () { $('option:selected', this).removeAttr("selected"); select_init($(this)); }); $(newRow).find('.pw_taxonomy_multiselect_search').each(function () { $('option:selected', this).removeAttr("selected"); $(this).select2_sortable(); }); // Reinitialise the field we previously destroyed $(newRow).prev().find('.pw_taxonomy_select_search').each(function () { select_init($(this)); }); // Reinitialise the field we previously destroyed $(newRow).prev().find('.pw_taxonomy_multiselect_search').each(function () { $(this).select2_sortable(); }); }); // Before a group row is shifted, destroy Select2. We'll reinitialise after the row shift $('.cmb-repeatable-group').on('cmb2_shift_rows_start', function (event, instance) { var groupWrap = $(instance).closest('.cmb-repeatable-group'); groupWrap.find('.pw_taxonomy_select2_search').each(function () { $(this).select2('destroy'); }); }); // When a group row is shifted, reinitialise Select2 $('.cmb-repeatable-group').on('cmb2_shift_rows_complete', function (event, instance) { var groupWrap = $(instance).closest('.cmb-repeatable-group'); groupWrap.find('.pw_taxonomy_select_search').each(function () { select_init($(this)); }); groupWrap.find('.pw_taxonomy_multiselect_search').each(function () { $(this).select2_sortable(); }); }); // Before a new repeatable field row is added, destroy Select2. We'll reinitialise after the row is added $('.cmb-add-row-button').on('click', function (event) { var $table = $(document.getElementById($(event.target).data('selector'))); var $oldRow = $table.find('.cmb-row').last(); $oldRow.find('.pw_taxonomy_select2_search').each(function () { $(this).select2('destroy'); }); }); // When a new repeatable field row is added, clear selection and initialise Select2 $('.cmb-repeat-table').on('cmb2_add_row', function (event, newRow) { // Reinitialise the field we previously destroyed $(newRow).prev().find('.pw_taxonomy_select_search').each(function () { $('option:selected', this).removeAttr("selected"); select_init($(this)); }); // Reinitialise the field we previously destroyed $(newRow).prev().find('.pw_taxonomy_multiselect_search').each(function () { $('option:selected', this).removeAttr("selected"); $(this).select2_sortable(); }); }); })(jQuery);