├── cmb2-field-ajax-search.php ├── css └── ajax-search.css ├── example.gif ├── js ├── ajax-search.js └── jquery.autocomplete.min.js └── readme.md /cmb2-field-ajax-search.php: -------------------------------------------------------------------------------- 1 | convert_as_id_css($field->_name()); 66 | $default_limit = 1; 67 | 68 | // Current filter is cmb2_render_{$object_to_search}_ajax_search ( post, user or term ) 69 | $object_to_search = str_replace( 'cmb2_render_', '', str_replace( '_ajax_search', '', current_filter() ) ); 70 | 71 | if( ! is_array( $value ) && strpos( $value, ', ' ) ) { 72 | $value = explode(', ', $value); 73 | } 74 | 75 | if( $field->args( 'multiple-item' ) == true ) { 76 | $default_limit = -1; // 0 or -1 means unlimited 77 | 78 | ?>input( array( 108 | 'type' => 'hidden', 109 | 'name' => $field->_name(), 110 | 'value' => $value, 111 | 'desc' => false 112 | ) ); 113 | 114 | $input_value = ( $value ? $this->object_text( $field_name, $value, $object_to_search ) : '' ); 115 | } 116 | 117 | echo $field_type->input( array( 118 | 'type' => 'text', 119 | 'name' => '_' . $field->_name(), 120 | 'id' => $field_name, 121 | 'class' => 'cmb-ajax-search cmb-' . $object_to_search . '-ajax-search', 122 | 'value' => $input_value, 123 | 'desc' => false, 124 | 'data-multiple' => $field->args( 'multiple-item' ) ? $field->args( 'multiple-item' ) : '0', 125 | 'data-limit' => $field->args( 'limit' ) ? $field->args( 'limit' ) : $default_limit, 126 | 'data-sortable' => $field->args( 'sortable' ) ? $field->args( 'sortable' ) : '0', 127 | 'data-object-type' => $object_to_search, 128 | 'data-query-args' => $field->args( 'query_args' ) ? htmlspecialchars( json_encode( $field->args( 'query_args' ) ), ENT_QUOTES, 'UTF-8' ) : '' 129 | ) ); 130 | 131 | echo ''; 132 | 133 | $field_type->_desc( true, true ); 134 | 135 | } 136 | 137 | /** 138 | * Display field 139 | */ 140 | public function display( $pre_output, $field, $display ) { 141 | $object_type = str_replace( 'cmb2_pre_field_display_', '', str_replace( '_ajax_search', '', current_filter() ) ); 142 | 143 | ob_start(); 144 | 145 | $field->peform_param_callback( 'before_display_wrap' ); 146 | 147 | printf( "
\n", $field->row_classes( 'display' ), $field->type() ); 148 | 149 | $field->peform_param_callback( 'before_display' ); 150 | 151 | if( is_array( $field->value ) ) : ?> 152 | value as $value ) : ?> 153 | 154 | object_text( $field->args['id'], $value, $object_type ); ?> 155 |
156 | 157 | 158 | 159 | object_text( $field->args['id'], $field->value, $object_type ); ?> 160 | 161 | peform_param_callback( 'after_display' ); 164 | 165 | echo "\n
"; 166 | 167 | $field->peform_param_callback( 'after_display_wrap' ); 168 | 169 | $pre_output = ob_get_clean(); 170 | 171 | return $pre_output; 172 | } 173 | 174 | /** 175 | * Optionally save the latitude/longitude values into two custom fields 176 | */ 177 | public function sanitize( $override_value, $value, $object_id, $field_args ) { 178 | if ( !is_array( $value ) || !( array_key_exists('repeatable', $field_args ) && $field_args['repeatable'] == TRUE ) ) { 179 | return $override_value; 180 | } 181 | 182 | $new_values = array(); 183 | foreach ( $value as $key => $val ) { 184 | $new_values[$key] = array_filter( array_map( 'sanitize_text_field', $val ) ); 185 | } 186 | 187 | return array_filter( array_values( $new_values ) ); 188 | } 189 | 190 | /** 191 | * Enqueue scripts and styles 192 | */ 193 | public function setup_admin_scripts() { 194 | 195 | wp_register_script( 'jquery-autocomplete-ajax-search', plugins_url( 'js/jquery.autocomplete.min.js', __FILE__ ), array( 'jquery' ), self::VERSION, true ); 196 | wp_register_script( 'cmb-ajax-search', plugins_url( 'js/ajax-search.js', __FILE__ ), array( 'jquery', 'jquery-autocomplete-ajax-search', 'jquery-ui-sortable' ), self::VERSION, true ); 197 | 198 | wp_localize_script( 'cmb-ajax-search', 'cmb_ajax_search', array( 199 | 'ajaxurl' => admin_url( 'admin-ajax.php' ), 200 | 'nonce' => wp_create_nonce( 'cmb_ajax_search_get_results' ), 201 | 'options' => apply_filters( 'cmb_field_ajax_search_autocomplete_options', array() ) 202 | ) ); 203 | 204 | wp_enqueue_script( 'cmb-ajax-search' ); 205 | wp_enqueue_style( 'cmb-ajax-search', plugins_url( 'css/ajax-search.css', __FILE__ ), array(), self::VERSION ); 206 | 207 | } 208 | 209 | /** 210 | * Ajax request : get results 211 | */ 212 | public function get_results() { 213 | $nonce = $_POST['nonce']; 214 | 215 | if ( ! wp_verify_nonce( $nonce, 'cmb_ajax_search_get_results' ) ) { 216 | // Wrong nonce 217 | die( json_encode( array( 218 | 'error' => __( 'Error : Unauthorized action' ) 219 | ) ) ); 220 | } else if ( ( ! isset( $_POST['field_id'] ) || empty( $_POST['field_id'] ) ) 221 | || ( ! isset( $_POST['object_type'] ) || empty( $_POST['object_type'] ) ) ) { 222 | // Wrong request parameters (field_id and object_type are mandatory) 223 | die( json_encode( array( 224 | 'error' => __( 'Error : Wrong request parameters' ) 225 | ) ) ); 226 | } else { 227 | $query_args = json_decode( stripslashes( htmlspecialchars_decode( $_POST['query_args'] ) ), true ); 228 | $data = array(); 229 | $results = array(); 230 | 231 | switch( $_POST['object_type'] ) { 232 | case 'post': 233 | $query_args['s'] = $_POST['query']; 234 | $query = new WP_Query( $query_args ); 235 | $results = $query->posts; 236 | break; 237 | case 'user': 238 | $query_args['search'] = '*' . $_POST['query'] . '*'; 239 | $query = new WP_User_Query( $query_args ); 240 | $results = $query->results; 241 | break; 242 | case 'term': 243 | $query_args['search'] = $_POST['query']; 244 | $query = new WP_Term_Query( $query_args ); 245 | $results = $query->terms; 246 | break; 247 | } 248 | 249 | foreach ( $results as $result ) : 250 | if( $_POST['object_type'] == 'term' ) { 251 | $result_id = $result->term_id; 252 | } else { 253 | $result_id = $result->ID; 254 | } 255 | 256 | $data[] = array( 257 | 'id' => $result_id, 258 | 'value' => $this->object_text( $_POST['field_id'], $result_id, $_POST['object_type'] ), 259 | 'link' => $this->object_link( $_POST['field_id'], $result_id, $_POST['object_type'] ) 260 | ); 261 | endforeach; 262 | 263 | wp_send_json( $data ); 264 | exit; 265 | } 266 | } 267 | 268 | public function object_text( $field_id, $object_id, $object_type ) { 269 | $text = ''; 270 | 271 | if( $object_type == 'post' ) { 272 | $text = get_the_title( $object_id ); 273 | } else if( $object_type == 'user' ) { 274 | $text = get_the_author_meta('display_name', $object_id); 275 | } else if( $object_type == 'term' ) { 276 | $term = get_term( $object_id ); 277 | 278 | $text = $term->name; 279 | } 280 | 281 | $text = apply_filters( "cmb_{$field_id}_ajax_search_result_text", $text, $object_id, $object_type ); 282 | 283 | return $text; 284 | } 285 | 286 | public function object_link( $field_id, $object_id, $object_type ) { 287 | $link = '#'; 288 | 289 | if( $object_type == 'post' ) { 290 | $link = get_edit_post_link( $object_id ); 291 | } else if( $object_type == 'user' ) { 292 | $link = get_edit_user_link( $object_id ); 293 | } else if( $object_type == 'term' ) { 294 | $link = get_edit_term_link( $object_id ); 295 | } 296 | 297 | $link = apply_filters( "cmb_{$field_id}_ajax_search_result_link", $link, $object_id, $object_type ); 298 | 299 | return $link; 300 | } 301 | 302 | } 303 | 304 | $cmb2_field_ajax_search = new CMB2_Field_Ajax_Search(); 305 | 306 | } 307 | 308 | -------------------------------------------------------------------------------- /css/ajax-search.css: -------------------------------------------------------------------------------- 1 | .autocomplete-suggestions { border: 1px solid #DDD; background: #FFF; overflow: auto; box-sizing: border-box; } 2 | .autocomplete-suggestion { padding: 5px 5px; white-space: nowrap; overflow: hidden; cursor: pointer; } 3 | .autocomplete-no-suggestion { padding: 5px 5px; white-space: nowrap; overflow: hidden; } 4 | .autocomplete-selected { background: #F0F0F0; } 5 | .autocomplete-suggestions strong { font-weight: normal; color: #3399FF; } 6 | .autocomplete-group { padding: 2px 5px; } 7 | .autocomplete-group strong { display: block; border-bottom: 1px solid #000; } 8 | 9 | input.cmb-ajax-search { width: 25em; } 10 | .cmb-ajax-search-results { width: 25em; padding: 0 1px; box-sizing: border-box; margin-bottom: 5px; } 11 | .cmb-ajax-search-results li { margin: 0; border-bottom: 1px solid #f4f4f4; padding: 7px 5px; background: #fff; } 12 | .cmb-ajax-search-results li:last-child { border-bottom: none; } 13 | .cmb-ajax-search-results li span.hndl { font-size: 13px; line-height: 16px; color: #ccc; margin: 0 8px 0 0; height: 16px; cursor: s-resize; } 14 | .cmb-ajax-search-results li span.hndl:before { content: '\2807'; } 15 | .cmb-ajax-search-results li a.edit-link { color: #333; text-decoration: none; } 16 | .cmb-ajax-search-results li a.edit-link:hover { color: #00a0d2; text-decoration: underline; } 17 | .cmb-ajax-search-results li a.remover { float: right; cursor:pointer; color: #ccc; } 18 | .cmb-ajax-search-results li a.remover span { height: 16px; line-height: 16px; font-size: 14px; } 19 | .cmb-ajax-search-results li a.remover span.dashicons-dismiss { display: none; } 20 | .cmb-ajax-search-results li a.remover:hover { color: #C00; } 21 | .cmb-ajax-search-results li a.remover:hover span.dashicons-dismiss { display: inline-block; } 22 | .cmb-ajax-search-results li a.remover:hover span.dashicons-no { display: none; } 23 | .cmb-ajax-search-results .ui-state-highlight { background: #f4f4f4; } 24 | .cmb-ajax-search-spinner { position: absolute; margin: 9px 0 0 -25px; width: 15px; display:none; } 25 | 26 | .columns-2 #postbox-container-1 .cmb-ajax-search-spinner { right: 25px; margin: -23px 0 0 0; } 27 | 28 | #side-sortables .cmb2-wrap input + input.cmb-ajax-search { margin-top: 1px; } /* Fix extra margin from CMB2 */ 29 | 30 | /* Repeatable groups */ 31 | .cmb-repeatable-group .cmb-type-post-ajax-search .cmb-td, .cmb-repeatable-group .cmb-type-user-ajax-search .cmb-td, .cmb-repeatable-group .cmb-type-term-ajax-search .cmb-td { position: relative; } 32 | .cmb-repeatable-group .cmb-ajax-search-spinner { position: absolute; top: 50%; right: 10px; margin: -7px 0 0 0; } 33 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubengc/cmb2-field-ajax-search/d6ce6047ff6f292e10fa6980d8e249b66261e576/example.gif -------------------------------------------------------------------------------- /js/ajax-search.js: -------------------------------------------------------------------------------- 1 | (function( document, $ ) { 2 | function init_ajax_search() { 3 | $('.cmb-ajax-search:not([data-ajax-search="true"])').each(function () { 4 | $(this).attr('data-ajax-search', true); 5 | 6 | var field_id = $(this).attr('id').replace(/[\[\]']+/g, '_'); // Field id, the true one field 7 | var object_type = $(this).attr('data-object-type'); 8 | var query_args = $(this).attr('data-query-args'); 9 | 10 | $(this).devbridgeAutocomplete(Object.assign({ 11 | serviceUrl: cmb_ajax_search.ajaxurl, 12 | type: 'POST', 13 | triggerSelectOnValidInput: false, 14 | showNoSuggestionNotice: true, 15 | params: { 16 | action : 'cmb_ajax_search_get_results', 17 | nonce : cmb_ajax_search.nonce, // nonce 18 | field_id : field_id, // Field id for hook purposes 19 | object_type : object_type, // post, user or term 20 | query_args : query_args, // Query args passed to field 21 | }, 22 | transformResult: function( results ) { 23 | var suggestions = $.parseJSON( results ); 24 | 25 | if( $('#' + field_id + '_results li').length ) { 26 | var selected_vals = []; 27 | var d = 0; 28 | 29 | $('#' + field_id + '_results input').each(function( index, element ) { 30 | selected_vals.push( $(this).val() ); 31 | }); 32 | 33 | // Remove already picked suggestions 34 | $(suggestions).each(function( index, suggestion ) { 35 | if( $.inArray( ( suggestion.id ).toString(), selected_vals ) > -1 ) { 36 | suggestions.splice( index - d, 1 ); 37 | d++; 38 | } 39 | }); 40 | } 41 | 42 | return { suggestions: suggestions }; 43 | }, 44 | onSearchStart: function(){ 45 | $(this).next('img.cmb-ajax-search-spinner').css( 'display', 'inline-block' ); 46 | }, 47 | onSearchComplete: function(){ 48 | $(this).next('img.cmb-ajax-search-spinner').hide(); 49 | }, 50 | onSelect: function ( suggestion ) { 51 | $(this).devbridgeAutocomplete('clearCache'); 52 | 53 | var field_name = $(this).attr('name'); 54 | var multiple = $(this).attr('data-multiple'); 55 | var limit = parseInt( $(this).attr('data-limit') ); 56 | var sortable = $(this).attr('data-sortable'); 57 | var field_name_temp = field_name.substring(1).replace( /[\[\]']+/g, '_' ); 58 | 59 | if( multiple == 1 ) { 60 | // Multiple 61 | $('#' + field_name_temp + '_results' ).append( '
  • ' + 62 | ( ( sortable == 1 ) ? '' : '' ) + 63 | '' + 64 | '' + suggestion.value + '' + 65 | '' + 66 | '
  • ' ); 67 | 68 | $(this).val( '' ); 69 | 70 | // Checks if there is the max allowed results, limit < 0 means unlimited 71 | if( limit > 0 && limit == $('#' + field_name_temp + '_results li').length ) { 72 | $(this).prop( 'disabled', 'disabled' ); 73 | } else { 74 | $(this).focus(); 75 | } 76 | } else { 77 | // Singular 78 | $('input[name="' + field_name + '"]').val( suggestion.id ).change(); 79 | } 80 | } 81 | }, 82 | cmb_ajax_search.options)); 83 | 84 | if( $(this).attr('data-sortable') == 1 ){ 85 | $('#' + field_id + '_results').sortable({ 86 | handle : '.hndl', 87 | placeholder : 'ui-state-highlight', 88 | forcePlaceholderSize : true 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | // Initialize ajax search 95 | init_ajax_search(); 96 | 97 | // Initialize on group fields add row 98 | $( document ).on( 'cmb2_add_row', function( evt, $row ) { 99 | $row.find('.cmb-ajax-search').attr('data-ajax-search', false); 100 | 101 | init_ajax_search(); 102 | }); 103 | 104 | // Initialize on widgets area 105 | $(document).on('widget-updated widget-added', function() { 106 | init_ajax_search(); 107 | }); 108 | 109 | // On click remover listener 110 | $('body').on( 'click', '.cmb-ajax-search-results a.remover', function() { 111 | $(this).parent('li').fadeOut( 400, function(){ 112 | var field_id = $(this).parents('ul').attr('id').replace('_results', ''); 113 | 114 | $('#' + field_id).removeProp( 'disabled' ); 115 | $('#' + field_id).devbridgeAutocomplete('clearCache'); 116 | 117 | $(this).remove(); 118 | }); 119 | }); 120 | })(document, jQuery); 121 | -------------------------------------------------------------------------------- /js/jquery.autocomplete.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ajax Autocomplete for jQuery, version 1.4.3 3 | * (c) 2017 Tomas Kirda 4 | * 5 | * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. 6 | * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete 7 | */ 8 | !function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports&&"function"==typeof require?require("jquery"):jQuery)}(function(a){"use strict";function b(c,d){var e=this;e.element=c,e.el=a(c),e.suggestions=[],e.badQueries=[],e.selectedIndex=-1,e.currentValue=e.element.value,e.timeoutId=null,e.cachedResponse={},e.onChangeTimeout=null,e.onChange=null,e.isLocal=!1,e.suggestionsContainer=null,e.noSuggestionsContainer=null,e.options=a.extend({},b.defaults,d),e.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"},e.hint=null,e.hintValue="",e.selection=null,e.initialize(),e.setOptions(d)}function c(a,b,c){return a.value.toLowerCase().indexOf(c)!==-1}function d(b){return"string"==typeof b?a.parseJSON(b):b}function e(a,b){if(!b)return a.value;var c="("+g.escapeRegExChars(b)+")";return a.value.replace(new RegExp(c,"gi"),"$1").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/<(\/?strong)>/g,"<$1>")}function f(a,b){return'
    '+b+"
    "}var g=function(){return{escapeRegExChars:function(a){return a.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")},createNode:function(a){var b=document.createElement("div");return b.className=a,b.style.position="absolute",b.style.display="none",b}}}(),h={ESC:27,TAB:9,RETURN:13,LEFT:37,UP:38,RIGHT:39,DOWN:40},i=a.noop;b.utils=g,a.Autocomplete=b,b.defaults={ajaxSettings:{},autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:e,formatGroup:f,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:i,onSearchComplete:i,onSearchError:i,preserveInput:!1,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0,preventBadQueries:!0,lookupFilter:c,paramName:"query",transformResult:d,showNoSuggestionNotice:!1,noSuggestionNotice:"No results",orientation:"bottom",forceFixPosition:!1},b.prototype={initialize:function(){var c,d=this,e="."+d.classes.suggestion,f=d.classes.selected,g=d.options;d.element.setAttribute("autocomplete","off"),d.noSuggestionsContainer=a('
    ').html(this.options.noSuggestionNotice).get(0),d.suggestionsContainer=b.utils.createNode(g.containerClass),c=a(d.suggestionsContainer),c.appendTo(g.appendTo||"body"),"auto"!==g.width&&c.css("width",g.width),c.on("mouseover.autocomplete",e,function(){d.activate(a(this).data("index"))}),c.on("mouseout.autocomplete",function(){d.selectedIndex=-1,c.children("."+f).removeClass(f)}),c.on("click.autocomplete",e,function(){d.select(a(this).data("index"))}),c.on("click.autocomplete",function(){clearTimeout(d.blurTimeoutId)}),d.fixPositionCapture=function(){d.visible&&d.fixPosition()},a(window).on("resize.autocomplete",d.fixPositionCapture),d.el.on("keydown.autocomplete",function(a){d.onKeyPress(a)}),d.el.on("keyup.autocomplete",function(a){d.onKeyUp(a)}),d.el.on("blur.autocomplete",function(){d.onBlur()}),d.el.on("focus.autocomplete",function(){d.onFocus()}),d.el.on("change.autocomplete",function(a){d.onKeyUp(a)}),d.el.on("input.autocomplete",function(a){d.onKeyUp(a)})},onFocus:function(){var a=this;a.fixPosition(),a.el.val().length>=a.options.minChars&&a.onValueChange()},onBlur:function(){var a=this;a.blurTimeoutId=setTimeout(function(){a.hide()},200)},abortAjax:function(){var a=this;a.currentRequest&&(a.currentRequest.abort(),a.currentRequest=null)},setOptions:function(b){var c=this,d=a.extend({},c.options,b);c.isLocal=Array.isArray(d.lookup),c.isLocal&&(d.lookup=c.verifySuggestionsFormat(d.lookup)),d.orientation=c.validateOrientation(d.orientation,"bottom"),a(c.suggestionsContainer).css({"max-height":d.maxHeight+"px",width:d.width+"px","z-index":d.zIndex}),this.options=d},clearCache:function(){this.cachedResponse={},this.badQueries=[]},clear:function(){this.clearCache(),this.currentValue="",this.suggestions=[]},disable:function(){var a=this;a.disabled=!0,clearTimeout(a.onChangeTimeout),a.abortAjax()},enable:function(){this.disabled=!1},fixPosition:function(){var b=this,c=a(b.suggestionsContainer),d=c.parent().get(0);if(d===document.body||b.options.forceFixPosition){var e=b.options.orientation,f=c.outerHeight(),g=b.el.outerHeight(),h=b.el.offset(),i={top:h.top,left:h.left};if("auto"===e){var j=a(window).height(),k=a(window).scrollTop(),l=-k+h.top-f,m=k+j-(h.top+g+f);e=Math.max(l,m)===l?"top":"bottom"}if("top"===e?i.top+=-f:i.top+=g,d!==document.body){var n,o=c.css("opacity");b.visible||c.css("opacity",0).show(),n=c.offsetParent().offset(),i.top-=n.top,i.left-=n.left,b.visible||c.css("opacity",o).hide()}"auto"===b.options.width&&(i.width=b.el.outerWidth()+"px"),c.css(i)}},isCursorAtEnd:function(){var a,b=this,c=b.el.val().length,d=b.element.selectionStart;return"number"==typeof d?d===c:!document.selection||(a=document.selection.createRange(),a.moveStart("character",-c),c===a.text.length)},onKeyPress:function(a){var b=this;if(!b.disabled&&!b.visible&&a.which===h.DOWN&&b.currentValue)return void b.suggest();if(!b.disabled&&b.visible){switch(a.which){case h.ESC:b.el.val(b.currentValue),b.hide();break;case h.RIGHT:if(b.hint&&b.options.onHint&&b.isCursorAtEnd()){b.selectHint();break}return;case h.TAB:if(b.hint&&b.options.onHint)return void b.selectHint();if(b.selectedIndex===-1)return void b.hide();if(b.select(b.selectedIndex),b.options.tabDisabled===!1)return;break;case h.RETURN:if(b.selectedIndex===-1)return void b.hide();b.select(b.selectedIndex);break;case h.UP:b.moveUp();break;case h.DOWN:b.moveDown();break;default:return}a.stopImmediatePropagation(),a.preventDefault()}},onKeyUp:function(a){var b=this;if(!b.disabled){switch(a.which){case h.UP:case h.DOWN:return}clearTimeout(b.onChangeTimeout),b.currentValue!==b.el.val()&&(b.findBestHint(),b.options.deferRequestBy>0?b.onChangeTimeout=setTimeout(function(){b.onValueChange()},b.options.deferRequestBy):b.onValueChange())}},onValueChange:function(){var b=this,c=b.options,d=b.el.val(),e=b.getQuery(d);return b.selection&&b.currentValue!==e&&(b.selection=null,(c.onInvalidateSelection||a.noop).call(b.element)),clearTimeout(b.onChangeTimeout),b.currentValue=d,b.selectedIndex=-1,c.triggerSelectOnValidInput&&b.isExactMatch(e)?void b.select(0):void(e.lengthh&&(c.suggestions=c.suggestions.slice(0,h)),c},getSuggestions:function(b){var c,d,e,f,g=this,h=g.options,i=h.serviceUrl;if(h.params[h.paramName]=b,h.onSearchStart.call(g.element,h.params)!==!1){if(d=h.ignoreParams?null:h.params,a.isFunction(h.lookup))return void h.lookup(b,function(a){g.suggestions=a.suggestions,g.suggest(),h.onSearchComplete.call(g.element,b,a.suggestions)});g.isLocal?c=g.getSuggestionsLocal(b):(a.isFunction(i)&&(i=i.call(g.element,b)),e=i+"?"+a.param(d||{}),c=g.cachedResponse[e]),c&&Array.isArray(c.suggestions)?(g.suggestions=c.suggestions,g.suggest(),h.onSearchComplete.call(g.element,b,c.suggestions)):g.isBadQuery(b)?h.onSearchComplete.call(g.element,b,[]):(g.abortAjax(),f={url:i,data:d,type:h.type,dataType:h.dataType},a.extend(f,h.ajaxSettings),g.currentRequest=a.ajax(f).done(function(a){var c;g.currentRequest=null,c=h.transformResult(a,b),g.processResponse(c,b,e),h.onSearchComplete.call(g.element,b,c.suggestions)}).fail(function(a,c,d){h.onSearchError.call(g.element,b,a,c,d)}))}},isBadQuery:function(a){if(!this.options.preventBadQueries)return!1;for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){var b=this,c=a(b.suggestionsContainer);a.isFunction(b.options.onHide)&&b.visible&&b.options.onHide.call(b.element,c),b.visible=!1,b.selectedIndex=-1,clearTimeout(b.onChangeTimeout),a(b.suggestionsContainer).hide(),b.signalHint(null)},suggest:function(){if(!this.suggestions.length)return void(this.options.showNoSuggestionNotice?this.noSuggestions():this.hide());var b,c=this,d=c.options,e=d.groupBy,f=d.formatResult,g=c.getQuery(c.currentValue),h=c.classes.suggestion,i=c.classes.selected,j=a(c.suggestionsContainer),k=a(c.noSuggestionsContainer),l=d.beforeRender,m="",n=function(a,c){var f=a.data[e];return b===f?"":(b=f,d.formatGroup(a,b))};return d.triggerSelectOnValidInput&&c.isExactMatch(g)?void c.select(0):(a.each(c.suggestions,function(a,b){e&&(m+=n(b,g,a)),m+='
    '+f(b,g,a)+"
    "}),this.adjustContainerWidth(),k.detach(),j.html(m),a.isFunction(l)&&l.call(c.element,j,c.suggestions),c.fixPosition(),j.show(),d.autoSelectFirst&&(c.selectedIndex=0,j.scrollTop(0),j.children("."+h).first().addClass(i)),c.visible=!0,void c.findBestHint())},noSuggestions:function(){var b=this,c=b.options.beforeRender,d=a(b.suggestionsContainer),e=a(b.noSuggestionsContainer);this.adjustContainerWidth(),e.detach(),d.empty(),d.append(e),a.isFunction(c)&&c.call(b.element,d,b.suggestions),b.fixPosition(),d.show(),b.visible=!0},adjustContainerWidth:function(){var b,c=this,d=c.options,e=a(c.suggestionsContainer);"auto"===d.width?(b=c.el.outerWidth(),e.css("width",b>0?b:300)):"flex"===d.width&&e.css("width","")},findBestHint:function(){var b=this,c=b.el.val().toLowerCase(),d=null;c&&(a.each(b.suggestions,function(a,b){var e=0===b.value.toLowerCase().indexOf(c);return e&&(d=b),!e}),b.signalHint(d))},signalHint:function(b){var c="",d=this;b&&(c=d.currentValue+b.value.substr(d.currentValue.length)),d.hintValue!==c&&(d.hintValue=c,d.hint=b,(this.options.onHint||a.noop)(c))},verifySuggestionsFormat:function(b){return b.length&&"string"==typeof b[0]?a.map(b,function(a){return{value:a,data:null}}):b},validateOrientation:function(b,c){return b=a.trim(b||"").toLowerCase(),a.inArray(b,["auto","bottom","top"])===-1&&(b=c),b},processResponse:function(a,b,c){var d=this,e=d.options;a.suggestions=d.verifySuggestionsFormat(a.suggestions),e.noCache||(d.cachedResponse[c]=a,e.preventBadQueries&&!a.suggestions.length&&d.badQueries.push(b)),b===d.getQuery(d.currentValue)&&(d.suggestions=a.suggestions,d.suggest())},activate:function(b){var c,d=this,e=d.classes.selected,f=a(d.suggestionsContainer),g=f.find("."+d.classes.suggestion);return f.find("."+e).removeClass(e),d.selectedIndex=b,d.selectedIndex!==-1&&g.length>d.selectedIndex?(c=g.get(d.selectedIndex),a(c).addClass(e),c):null},selectHint:function(){var b=this,c=a.inArray(b.hint,b.suggestions);b.select(c)},select:function(a){var b=this;b.hide(),b.onSelect(a)},moveUp:function(){var b=this;if(b.selectedIndex!==-1)return 0===b.selectedIndex?(a(b.suggestionsContainer).children().first().removeClass(b.classes.selected),b.selectedIndex=-1,b.el.val(b.currentValue),void b.findBestHint()):void b.adjustScroll(b.selectedIndex-1)},moveDown:function(){var a=this;a.selectedIndex!==a.suggestions.length-1&&a.adjustScroll(a.selectedIndex+1)},adjustScroll:function(b){var c=this,d=c.activate(b);if(d){var e,f,g,h=a(d).outerHeight();e=d.offsetTop,f=a(c.suggestionsContainer).scrollTop(),g=f+c.options.maxHeight-h,eg&&a(c.suggestionsContainer).scrollTop(e-c.options.maxHeight+h),c.options.preserveInput||c.el.val(c.getValue(c.suggestions[b].value)),c.signalHint(null)}},onSelect:function(b){var c=this,d=c.options.onSelect,e=c.suggestions[b];c.currentValue=c.getValue(e.value),c.currentValue===c.el.val()||c.options.preserveInput||c.el.val(c.currentValue),c.signalHint(null),c.suggestions=[],c.selection=e,a.isFunction(d)&&d.call(c.element,e)},getValue:function(a){var b,c,d=this,e=d.options.delimiter;return e?(b=d.currentValue,c=b.split(e),1===c.length?a:b.substr(0,b.length-c[c.length-1].length)+a):a},dispose:function(){var b=this;b.el.off(".autocomplete").removeData("autocomplete"),a(window).off("resize.autocomplete",b.fixPositionCapture),a(b.suggestionsContainer).remove()}},a.fn.devbridgeAutocomplete=function(c,d){var e="autocomplete";return arguments.length?this.each(function(){var f=a(this),g=f.data(e);"string"==typeof c?g&&"function"==typeof g[c]&&g[c](d):(g&&g.dispose&&g.dispose(),g=new b(this,c),f.data(e,g))}):this.first().data(e)},a.fn.autocomplete||(a.fn.autocomplete=a.fn.devbridgeAutocomplete)}); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | CMB2 Field Type: Ajax Search 2 | ================== 3 | 4 | Custom fields for [CMB2](https://github.com/WebDevStudios/CMB2) to attach posts, users or terms to each others. 5 | 6 | ![example](example.gif) 7 | 8 | Once activated, this plugin adds three new field types `post_ajax_search`, `user_ajax_search` and `term_ajax_search`. 9 | 10 | This plugin is an update of [CMB2 Field Type: Post Search Ajax](https://github.com/alexis-magina/cmb2-field-post-search-ajax) by [Magina](http://magina.fr/) with support to attach posts, users or terms. 11 | 12 | ## Installation 13 | 14 | You can install this field type as you would a WordPress plugin: 15 | 16 | - Download the plugin 17 | - Place the plugin folder in your /wp-content/plugins/ directory 18 | - Activate the plugin in the Plugin dashboard 19 | 20 | ## Parameters 21 | 22 | Options : 23 | - multiple (bool, default = false) : Turn field into a multiple attached objects 24 | - limit (int, default = -1 : single selection) : Limit the number of posts that can be selected (-1 for unlimited) 25 | - sortable (bool, default = false) : Allow selected items to be sort (only if multiple = true) 26 | - query_args (array) : Query arguments to pass on each request 27 | 28 | Query args: 29 | - query_args accepts same parameters as [WP_Query](https://codex.wordpress.org/Class_Reference/WP_Query) for `post_ajax_search` 30 | - query_args accepts same parameters as [WP_User_Query](https://codex.wordpress.org/Class_Reference/WP_User_Query) for `user_ajax_search` 31 | - query_args accepts same parameters as [WP_Term_Query](https://developer.wordpress.org/reference/classes/wp_term_query/) for `term_ajax_search` 32 | 33 | ## Examples 34 | 35 | ```php 36 | add_action( 'cmb2_admin_init', 'cmb2_ajax_search_metabox' ); 37 | function cmb2_ajax_search_metabox() { 38 | 39 | $prefix = 'your_prefix_demo_'; 40 | 41 | $cmb_demo = new_cmb2_box( array( 42 | 'id' => $prefix . 'metabox', 43 | 'title' => __( 'Attached posts Metabox', 'cmb2' ), 44 | 'object_types' => array( 'page', 'post' ), // Post type 45 | ) ); 46 | 47 | // Single post 48 | $cmb_demo->add_field( array( 49 | 'name' => __( 'Attached post', 'cmb2' ), 50 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 51 | 'id' => $prefix . 'post', 52 | 'type' => 'post_ajax_search', 53 | 'query_args' => array( 54 | 'post_type' => array( 'post' ), 55 | 'posts_per_page' => -1 56 | ) 57 | ) ); 58 | 59 | // Multiple posts 60 | $cmb_demo->add_field( array( 61 | 'name' => __( 'Multiple posts', 'cmb2' ), 62 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 63 | 'id' => $prefix . 'posts', 64 | 'type' => 'post_ajax_search', 65 | 'multiple-item' => true, 66 | 'limit' => 10, 67 | 'query_args' => array( 68 | 'post_type' => array( 'post', 'page' ), 69 | 'post_status' => array( 'publish', 'pending' ) 70 | ) 71 | ) ); 72 | 73 | // Single user 74 | $cmb_demo->add_field( array( 75 | 'name' => __( 'Attached user', 'cmb2' ), 76 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 77 | 'id' => $prefix . 'user', 78 | 'type' => 'user_ajax_search', 79 | 'query_args' => array( 80 | 'role' => array( 'Subscriber' ), 81 | 'search_columns' => array( 'user_login', 'user_email' ) 82 | ) 83 | ) ); 84 | 85 | // Multiple users 86 | $cmb_demo->add_field( array( 87 | 'name' => __( 'Multiple users', 'cmb2' ), 88 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 89 | 'id' => $prefix . 'users', 90 | 'type' => 'user_ajax_search', 91 | 'multiple-item' => true, 92 | 'limit' => 5, 93 | 'query_args' => array( 94 | 'role__not_in' => array( 'Administrator', 'Editor' ), 95 | ) 96 | ) ); 97 | 98 | // Single term 99 | $cmb_demo->add_field( array( 100 | 'name' => __( 'Attached term', 'cmb2' ), 101 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 102 | 'id' => $prefix . 'term', 103 | 'type' => 'term_ajax_search', 104 | 'query_args' => array( 105 | 'taxonomy' => 'category', 106 | 'childless' => true 107 | ) 108 | ) ); 109 | 110 | // Multiple terms 111 | $cmb_demo->add_field( array( 112 | 'name' => __( 'Multiple terms', 'cmb2' ), 113 | 'desc' => __( 'Field description (optional)', 'cmb2' ), 114 | 'id' => $prefix . 'terms', 115 | 'type' => 'term_ajax_search', 116 | 'multiple-item' => true, 117 | 'limit' => -1, 118 | 'query_args' => array( 119 | 'taxonomy' => 'post_tag', 120 | 'hide_empty' => false 121 | ) 122 | ) ); 123 | 124 | } 125 | ``` 126 | 127 | ## Customize results output 128 | 129 | You can use `cmb_{$field_id}_ajax_search_result_text` to customize the text returned from ajax searches and `cmb_{$field_id}_ajax_search_result_link` to customize the link, check next example: 130 | 131 | ```php 132 | add_filter( 'cmb_your_prefix_demo_posts_ajax_search_result_text', 'cmb2_ajax_search_custom_field_text', 10, 3 ); 133 | function cmb2_ajax_search_custom_field_text( $text, $object_id, $object_type ) { 134 | $text = sprintf( '#%s - %s', $object_id, $text ); // #123 - Post title 135 | 136 | return $text; 137 | } 138 | 139 | add_filter( 'cmb_your_prefix_demo_posts_ajax_search_result_link', 'cmb2_ajax_search_custom_field_link', 10, 3 ); 140 | function cmb2_ajax_search_custom_field_link( $link, $object_id, $object_type ) { 141 | if( $object_id == 123 ) { 142 | $link = '#'; 143 | } 144 | 145 | return $link; 146 | } 147 | ``` 148 | 149 | ## Retrieve the field value 150 | 151 | If multiple == false will return the ID of attached object: 152 | `get_post_meta( get_the_ID(), 'your_field_id', true );` 153 | 154 | If multiple == true will return an array of IDs of attached object: 155 | `get_post_meta( get_the_ID(), 'your_field_id', false );` 156 | 157 | ## Changelog 158 | 159 | ### 1.0.3 160 | 161 | * Fixed issues with repeatable fields 162 | * Removed unused code 163 | * Moved to new paramenter `multiple-item` to avoid conflicts with CMB2 164 | 165 | ### 1.0.2 166 | * Updated devbridgeAutocomplete lib 167 | 168 | ### 1.0.1 169 | * Group fields support 170 | * Widget area support 171 | * Use of devbridgeAutocomplete() instead of autocomplete() to avoid errors 172 | 173 | ### 1.0.0 174 | * Initial commit 175 | --------------------------------------------------------------------------------