diff --git a/cmb2-attached-posts-field.php b/cmb2-attached-posts-field.php index 4826143..2031cad 100644 --- a/cmb2-attached-posts-field.php +++ b/cmb2-attached-posts-field.php @@ -3,9 +3,8 @@ * Plugin Name: CMB2 Field Type: Attached Posts * Plugin URI: https://github.com/WebDevStudios/cmb2-attached-posts * Description: Attached posts field type for CMB2. - * Version: 1.2.7 - * Author: WebDevStudios - * Author URI: http://webdevstudios.com + * Version: 2.1 + * Author: WebDevStudios, Ipstenu * License: GPLv2+ */ @@ -18,8 +17,6 @@ * @package WDS_CMB2_Attached_Posts_Field * @author WebDevStudios * @copyright 2016 WebDevStudios - * @license GPL-2.0+ - * @version 1.2.7 * @link https://github.com/WebDevStudios/cmb2-attached-posts * @since 1.2.3 */ @@ -46,7 +43,7 @@ * Loader versioning: http://jtsternberg.github.io/wp-lib-loader/ */ -if ( ! class_exists( 'WDS_CMB2_Attached_Posts_Field_127', false ) ) { +if ( ! class_exists( 'WDS_CMB2_Attached_Posts_Field_210', false ) ) { /** * Versioned loader class-name @@ -57,18 +54,18 @@ * @package WDS_CMB2_Attached_Posts_Field * @author WebDevStudios * @license GPL-2.0+ - * @version 1.2.7 + * @version 2.1.0 * @link https://github.com/WebDevStudios/cmb2-attached-posts * @since 1.2.3 */ - class WDS_CMB2_Attached_Posts_Field_127 { + class WDS_CMB2_Attached_Posts_Field_210 { /** * WDS_CMB2_Attached_Posts_Field version number * @var string * @since 1.2.3 */ - const VERSION = '1.2.7'; + const VERSION = '2.1'; /** * Current version hook priority. @@ -155,5 +152,5 @@ public function include_lib() { } // Kick it off. - new WDS_CMB2_Attached_Posts_Field_127; + new WDS_CMB2_Attached_Posts_Field_210(); } diff --git a/init.php b/init.php index 3b65099..c2fbfba 100644 --- a/init.php +++ b/init.php @@ -59,31 +59,38 @@ protected function __construct() { */ public function render( $field, $escaped_value, $object_id, $object_type, $field_type ) { self::setup_scripts(); - $this->field = $field; + $this->field = $field; $this->do_type_label = false; if ( ! is_admin() ) { // Will need custom styling! // @todo add styles for front-end - require_once( ABSPATH . 'wp-admin/includes/template.php' ); + require_once ABSPATH . 'wp-admin/includes/template.php'; do_action( 'cmb2_attached_posts_field_add_find_posts_div' ); } else { // markup needed for modal add_action( 'admin_footer', 'find_posts_div' ); } - $query_args = (array) $this->field->options( 'query_args' ); + $query_args = (array) $this->field->options( 'query_args' ); $query_users = $this->field->options( 'query_users' ); + $post_status = array( 'publish' ); if ( ! $query_users ) { // Setup our args - $args = wp_parse_args( $query_args, array( - 'post_type' => 'post', - 'posts_per_page' => 100, - ) ); + $args = wp_parse_args( + $query_args, + array( + 'post_type' => 'post', + 'posts_per_page' => apply_filters( 'cmb2_attached_posts_per_page_filter', 50 ), + 'post_status' => apply_filters( 'cmb2_attached_posts_status_filter', $post_status ), + ) + ); + // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $_POST['post'] ) ) { + // phpcs:ignore WordPress.Security.NonceVerification $args['post__not_in'] = array( absint( $_POST['post'] ) ); } @@ -112,15 +119,19 @@ public function render( $field, $escaped_value, $object_id, $object_type, $field } else { // Setup our args - $args = wp_parse_args( $query_args, array( - 'number' => 100, - ) ); + $args = wp_parse_args( + $query_args, + array( + 'number' => 100, + ) + ); $post_type_labels = $field_type->_text( 'users_text', esc_html__( 'Users' ) ); } $filter_boxes = ''; // Check 'filter' setting if ( $this->field->options( 'filter_boxes' ) ) { + // translators: %s is the type of content we are filtering. $filter_boxes = '
'; } @@ -131,25 +142,32 @@ public function render( $field, $escaped_value, $object_id, $object_type, $field // If there are no posts found, just stop if ( empty( $objects ) ) { + echo '

No entries found for the ' . esc_html( $post_type_labels ) . ' post types

'; return; } // Wrap our lists - echo '
'; + echo '
'; // Open our retrieved, or found posts, list echo '
'; - echo '

' . sprintf( __( 'Available %s', 'cmb' ), $post_type_labels ) . '

'; + // translators: %s is the post type. + echo '

' . esc_html( sprintf( __( 'Newest %s', 'cmb' ), $post_type_labels ) ) . '

'; // Set .has_thumbnail $has_thumbnail = $this->field->options( 'show_thumbnails' ) ? ' has-thumbnails' : ''; $hide_selected = $this->field->options( 'hide_selected' ) ? ' hide-selected' : ''; + // Check if we have a max limit AND if we reached it. + $has_max_limit = ( $this->field->attributes( 'data-max-items' ) ) ? (int) $this->field->attributes( 'data-max-items' ) : false; + $reached_limit = ( false !== $has_max_limit && count( $attached ) === $has_max_limit ) ? ' has-reached-limit' : ''; + if ( $filter_boxes ) { + // phpcs:ignore printf( $filter_boxes, 'available-search' ); } - echo '
    '; + echo '
      '; // Loop through our posts as list items $this->display_retrieved( $objects, $attached ); @@ -159,33 +177,46 @@ public function render( $field, $escaped_value, $object_id, $object_type, $field // @todo make User search work. if ( ! $query_users ) { - $findtxt = $field_type->_text( 'find_text', __( 'Search' ) ); - - $js_data = json_encode( array( - 'queryUsers' => $query_users, - 'types' => $query_users ? 'user' : (array) $args['post_type'], - 'cmbId' => $this->field->cmb_id, - 'errortxt' => esc_attr( $field_type->_text( 'error_text', __( 'An error has occurred. Please reload the page and try again.' ) ) ), - 'findtxt' => esc_attr( $field_type->_text( 'find_text', __( 'Find Posts or Pages' ) ) ), - 'groupId' => $this->field->group ? $this->field->group->id() : false, - 'fieldId' => $this->field->_id(), - 'exclude' => isset( $args['post__not_in'] ) ? $args['post__not_in'] : array(), - ) ); - - echo '

      '; + // Set defaults. + $search_txt = $field_type->_text( 'find_text', __( 'Search' ) ); + $find_txt = __( 'Find Posts and Pages' ); + + // if the post type value isn't an array, let the post type decide what it is. + if ( ! is_array( $args['post_type'] ) ) { + $post_type_obj = get_post_type_object( $args['post_type'] ); + $find_txt = $post_type_obj->labels->search_items; + } + + // Build JS. + $js_data = wp_json_encode( + array( + 'queryUsers' => $query_users, + 'types' => $query_users ? 'user' : (array) $args['post_type'], + 'cmbId' => $this->field->cmb_id, + 'errortxt' => esc_attr( $field_type->_text( 'error_text', __( 'An error has occurred. Please reload the page and try again.' ) ) ), + 'findtxt' => esc_attr( $field_type->_text( 'find_text', $find_txt ) ), + 'groupId' => $this->field->group ? $this->field->group->id() : false, + 'fieldId' => $this->field->_id(), + 'exclude' => isset( $args['post__not_in'] ) ? $args['post__not_in'] : array(), + ) + ); + + echo '

      '; } echo '
'; // Open our attached posts list echo '
'; - echo '

' . sprintf( __( 'Attached %s', 'cmb' ), $post_type_labels ) . '

'; + // translators: %s is the post content. + echo '

' . esc_html( sprintf( __( 'Attached %s', 'cmb' ), $post_type_labels ) ) . '

'; if ( $filter_boxes ) { + // phpcs:ignore printf( $filter_boxes, 'attached-search' ); } - echo '
    '; + echo '
      '; // If we have any ids saved already, display them $ids = $this->display_attached( $attached ); @@ -194,12 +225,15 @@ public function render( $field, $escaped_value, $object_id, $object_type, $field echo '
    '; echo '
'; - echo $field_type->input( array( - 'type' => 'hidden', - 'class' => 'attached-posts-ids', - 'value' => ! empty( $ids ) ? implode( ',', $ids ) : '', - 'desc' => '', - ) ); + // phpcs:ignore + echo $field_type->input( + array( + 'type' => 'hidden', + 'class' => 'attached-posts-ids', + 'value' => ! empty( $ids ) ? implode( ',', $ids ) : '', // phpcs:ignore + 'desc' => '', + ) + ); echo '
'; @@ -224,10 +258,11 @@ protected function display_retrieved( $objects, $attached ) { foreach ( $objects as $object ) { // Set our zebra stripes - $class = ++$count % 2 == 0 ? 'even' : 'odd'; + ++$count; // increment count. + $class = ( 0 === $count % 2 ) ? 'even' : 'odd'; // Set a class if our post is in our attached meta - $class .= ! empty ( $attached ) && in_array( $this->get_id( $object ), $attached ) ? ' added' : ''; + $class .= ! empty( $attached ) && in_array( $this->get_id( $object ), $attached ) ? ' added' : ''; $this->list_item( $object, $class ); } @@ -264,7 +299,8 @@ protected function display_attached( $attached ) { } // Set our zebra stripes - $class = ++$count % 2 == 0 ? 'even' : 'odd'; + ++$count; + $class = ( 0 === $count % 2 ) ? 'even' : 'odd'; $this->list_item( $object, $class, 'dashicons-minus' ); $ids[ $id ] = $id; @@ -287,14 +323,14 @@ protected function display_attached( $attached ) { public function list_item( $object, $li_class, $icon_class = 'dashicons-plus' ) { // Build our list item printf( - '
  • %3$s%5$s%6$s
  • ', - $this->get_id( $object ), - $li_class, - $this->get_thumb( $object ), - $this->get_edit_link( $object ), - $this->get_title( $object ), - $this->get_object_label( $object ), - $icon_class + '
  • %3$s%5$s%6$s
  • ', + esc_attr( $this->get_id( $object ) ), + esc_attr( $li_class ), + esc_html( $this->get_thumb( $object ) ), + esc_url( $this->get_edit_link( $object ) ), + esc_html( $this->get_title( $object ) ), + esc_html( $this->get_object_label( $object ) ), + esc_attr( $icon_class ) ); } @@ -330,7 +366,7 @@ public function get_thumb( $object ) { * @return int The object ID. */ public function get_id( $object ) { - return $object->ID; + return isset( $object->ID ) ? $object->ID : false; } /** @@ -343,9 +379,12 @@ public function get_id( $object ) { * @return string The object title. */ public function get_title( $object ) { - return $this->field->options( 'query_users' ) - ? $object->data->display_name - : get_the_title( $object ); + $post = get_post( $object ); + // Initial Title + $title = $this->field->options( 'query_users' ) ? $object->data->display_name : get_the_title( $post->ID ); + $title = apply_filters( 'cmb2_attached_posts_title_filter', $title, $post->ID ); + + return $title; } /** @@ -363,7 +402,7 @@ public function get_object_label( $object ) { } $post_type_obj = get_post_type_object( $object->post_type ); - $label = isset( $post_type_obj->labels->singular_name ) ? $post_type_obj->labels->singular_name : $post_type_obj->label; + $label = isset( $post_type_obj->labels->singular_name ) ? $post_type_obj->labels->singular_name : $post_type_obj->label; return apply_filters( 'cmb2_attached_posts_field_label', ' — ' . $label . ' ', $label, $object ); } @@ -417,8 +456,8 @@ public function get_all_objects( $args, $attached = array() ) { } if ( ! empty( $attached ) ) { - $is_users = $this->field->options( 'query_users' ); - $args[ $is_users ? 'include' : 'post__in' ] = $attached; + $is_users = $this->field->options( 'query_users' ); + $args[ $is_users ? 'include' : 'post__in' ] = $attached; $args[ $is_users ? 'number' : 'posts_per_page' ] = count( $attached ); $new = $this->get_objects( $args ); @@ -430,11 +469,11 @@ public function get_all_objects( $args, $attached = array() ) { } } - return $attached_objects; + return apply_filters( 'cmb2_attached_posts_objects', $attached_objects, $args ); } /** - * Peforms a get_posts or get_users query. + * Performs a get_posts or get_users query. * * @since 1.2.4 * @@ -458,7 +497,7 @@ protected static function setup_scripts() { // Windows $content_dir = str_replace( '/', DIRECTORY_SEPARATOR, WP_CONTENT_DIR ); $content_url = str_replace( $content_dir, WP_CONTENT_URL, $dir ); - $url = str_replace( DIRECTORY_SEPARATOR, '/', $content_url ); + $url = str_replace( DIRECTORY_SEPARATOR, '/', $content_url ); } else { $url = str_replace( @@ -481,14 +520,18 @@ protected static function setup_scripts() { 'wp-backbone', ); - wp_enqueue_script( 'cmb2-attached-posts-field', $url . 'js/attached-posts.js', $requirements, self::VERSION, true ); - wp_enqueue_style( 'cmb2-attached-posts-field', $url . 'css/attached-posts-admin.css', array(), self::VERSION ); + wp_enqueue_script( 'cmb2-attached-posts-field', plugins_url( 'js/attached-posts.js', __FILE__ ), $requirements, self::VERSION, true ); + wp_enqueue_style( 'cmb2-attached-posts-field', plugins_url( 'css/attached-posts-admin.css', __FILE__ ), array(), self::VERSION ); if ( ! $once ) { - wp_localize_script( 'cmb2-attached-posts-field', 'CMBAP', array( - 'edit_link_template' => str_replace( get_the_ID(), 'REPLACEME', get_edit_post_link( get_the_ID() ) ), - 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), - ) ); + wp_localize_script( + 'cmb2-attached-posts-field', + 'CMBAP', + array( + 'edit_link_template' => str_replace( get_the_ID(), 'REPLACEME', get_edit_post_link( get_the_ID() ) ), + 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), + ) + ); $once = true; } @@ -529,11 +572,12 @@ public function sanitize( $sanitized_val, $val ) { * @return void */ public function ajax_find_posts() { + // @codingStandardsIgnoreStart if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['cmb2_attached_search'], $_POST['retrieved'], $_POST['action'], $_POST['search_types'] ) - && 'find_posts' == $_POST['action'] + && 'find_posts' === $_POST['action'] && ! empty( $_POST['search_types'] ) ) { // This is not working until we fix the user query bit. @@ -543,6 +587,7 @@ public function ajax_find_posts() { add_action( 'pre_get_posts', array( $this, 'modify_query' ) ); } } + // @codingStandardsIgnoreEnd } /** @@ -555,10 +600,12 @@ public function ajax_find_posts() { * @return void */ public function modify_query( $query ) { + // @codingStandardsIgnoreStart $is_users = 'pre_get_users' === current_filter(); if ( $is_users ) { // This is not working until we fix the user query bit. + return; } else { $types = $_POST['search_types']; $types = is_array( $types ) ? array_map( 'esc_attr', $types ) : esc_attr( $types ); @@ -572,13 +619,14 @@ public function modify_query( $query ) { if ( ! empty( $_POST['exclude'] ) && is_array( $_POST['exclude'] ) ) { // Exclude the post that we're looking at. $exclude = array_map( 'absint', $_POST['exclude'] ); - $ids = array_merge( $ids, $exclude ); + $ids = array_merge( $ids, $exclude ); } $query->set( $is_users ? 'exclude' : 'post__not_in', $ids ); } $this->maybe_callback( $query, $_POST ); + // @codingStandardsIgnoreEnd } /** @@ -606,9 +654,11 @@ public function maybe_callback( $query, $post_args ) { $field = $cmb->get_field( $field, $group ); } - if ( $field && ( $cb = $field->maybe_callback( 'attached_posts_search_query_cb' ) ) ) { + $cb = $field->maybe_callback( 'attached_posts_search_query_cb' ); + if ( $field && $cb ) { call_user_func( $cb, $query, $field, $this ); } + } /** @@ -636,7 +686,7 @@ public function display_render( $pre_output, $field, $display ) { // Then loop and output. foreach ( $display->field->value as $val ) { - $rows[] = $this->_display_render( $display->field, $val ); + $rows[] = $this->re_display_render( $display->field, $val ); } } @@ -651,9 +701,8 @@ public function display_render( $pre_output, $field, $display ) { } else { $return .= '—'; } - } else { - $return .= $this->_display_render( $display->field, $display->field->value ); + $return .= $this->re_display_render( $display->field, $display->field->value ); } return $return ? $return : $pre_output; @@ -667,15 +716,15 @@ public function display_render( $pre_output, $field, $display ) { * @param CMB2_Field $field This field object. * @param mixed $val The field value. */ - public function _display_render( $field, $val ) { + public function re_display_render( $field, $val ) { $return = ''; - $posts = array(); + $posts = array(); if ( ! empty( $val ) ) { foreach ( (array) $val as $id ) { $title = get_the_title( $id ); if ( $title ) { - $edit_link = get_edit_post_link( $id ); + $edit_link = get_edit_post_link( $id ); $posts[ $id ] = compact( 'title', 'edit_link' ); } } @@ -685,11 +734,14 @@ public function _display_render( $field, $val ) { $return .= '
      '; foreach ( (array) $posts as $id => $post ) { + + $title = apply_filters( 'cmb2_attached_posts_title_filter', $post['title'], $id ); + $return .= sprintf( '
    1. %s
    2. ', $id, $post['edit_link'], - $post['title'] + $title ); } $return .= '
    '; @@ -700,5 +752,6 @@ public function _display_render( $field, $val ) { return $return; } + } WDS_CMB2_Attached_Posts_Field::get_instance(); diff --git a/js/attached-posts.js b/js/attached-posts.js index 02e4b16..7cfc993 100755 --- a/js/attached-posts.js +++ b/js/attached-posts.js @@ -12,8 +12,51 @@ window.CMBAP = window.CMBAP || {}; app.$.retrievedPosts = $wrap.find( '.retrieved' ); app.$.attachedPosts = $wrap.find( '.attached' ); app.doType = $wrap.find( '.object-label' ).length; + app.$.attachedBoxes = $wrap.length; // Check for multiple attached-post meta boxes }; + // Helper function to determine wether we have exceeded our limit or not. + app._hasExceeded = function ($wrap) { + var $input = app.getPostIdsInput($wrap); + var maxItems = $input.data('max-items'); + var currentNumberItems = app.getPostIdsVal($input).length; + + return (typeof maxItems !== "undefined") && currentNumberItems >= maxItems; + }; + + // Make sure we cannot attach more posts than we would like. + app.updateReadOnly = function ($wrap) { + + // IF we have exceeded our limit, then ensure the user cannot attach more items. + // ELSE, make sure the user can attach items. + if (app._hasExceeded($wrap)) { + // Also ensure items aren't draggable + // @see https://stackoverflow.com/questions/1324044/how-do-i-disable-a-jquery-ui-draggable + // app.$retrievedPosts().draggable('disable'); + $wrap.find('.attached').droppable("option", "accept", ".doesnotexist"); + } else { + // app.$retrievedPosts().draggable('enable'); + $wrap.find('.attached').droppable("option", "accept", ".retrieved li"); + } + }; + + app.updateRemaining = function ($wrap) { + var $input = app.getPostIdsInput($wrap); + var currentNumberItems = app.getPostIdsVal($input).length; + var maxItems = $input.data('max-items'); + var $remainingLabel = $wrap.find('.attached-posts-remaining'); + var $remainingNumberLabel = $remainingLabel.find('.attached-posts-remaining-number'); + var remainingNumber = 0; + + if (typeof maxItems !== "undefined") { + // How many can we add? + remainingNumber = maxItems - currentNumberItems; + + // Show the label and update the number inside + $remainingLabel.removeClass("hidden"); + $remainingNumberLabel.html(remainingNumber); + } + }; app.init = function() { app.cache(); @@ -23,6 +66,14 @@ window.CMBAP = window.CMBAP || {}; // Allow the right list to be droppable and sortable app.makeDroppable(); + + $(".attached-posts-wrap").each(function () { + // Update whether or not the lists should be editable by the user. This is usually when a user has attached too many items. + app.updateReadOnly($(this)); + + app.updateRemaining($(this)) + }); + $( '.cmb2-wrap > .cmb2-metabox' ) // Add posts when the plus icon is clicked .on( 'click', '.attached-posts-wrap .retrieved .add-remove', app._moveRowToAttached ) @@ -48,7 +99,7 @@ window.CMBAP = window.CMBAP || {}; app.makeDroppable = function() { app.$.attachedPosts.droppable({ accept: '.retrieved li', - drop: function(evt, ui) { + drop: function( evt, ui ) { app.buildItems( ui.draggable ); } }).sortable({ @@ -75,6 +126,8 @@ window.CMBAP = window.CMBAP || {}; item.clone().appendTo( $wrap.find( '.attached' ) ); app.resetAttachedListItems( $wrap ); + app.updateRemaining($wrap); + }; // Add the items when the plus icon is clicked @@ -87,6 +140,10 @@ window.CMBAP = window.CMBAP || {}; var itemID = $li.data( 'id' ); var $wrap = $li.parents( '.attached-posts-wrap' ); + if (app._hasExceeded($wrap)) { + return; + } + if ( $li.hasClass( 'added' ) ) { return; } @@ -103,6 +160,8 @@ window.CMBAP = window.CMBAP || {}; $wrap.find( '.attached' ).append( $li.clone() ); app.resetAttachedListItems( $wrap ); + app.updateReadOnly($wrap); + app.updateRemaining($wrap); }; // Remove items from our attached list when the minus icon is clicked @@ -123,6 +182,8 @@ window.CMBAP = window.CMBAP || {}; $wrap.find('.retrieved li[data-id="' + itemID +'"]').removeClass('added'); app.resetAttachedListItems( $wrap ); + app.updateReadOnly($wrap); + app.updateRemaining($wrap); }; app.inputHasId = function( $wrap, itemID ) { @@ -376,6 +437,7 @@ window.CMBAP = window.CMBAP || {}; }; app._openSearch = function( evt ) { + app.updateRetrievedPosts( $(this) ); app.openSearch( $( evt.currentTarget ) ); }; @@ -388,6 +450,14 @@ window.CMBAP = window.CMBAP || {}; app.search.trigger( 'open' ); }; + app.updateRetrievedPosts = function( $this ){ + // If more than one custom_attached_posts update cache with current metabox context + if ( app.$.attachedBoxes > 1 ) { + var $wrap = $this.closest(' .attached-posts-wrap' ); + app.$.retrievedPosts = $wrap.find( '.retrieved' ); + } + } + $( app.init ); } )( window, document, jQuery, window.CMBAP );