Deligraph-V3/vendor/johnbillion/extended-cpts/src/TaxonomyAdmin.php

664 lines
20 KiB
PHP

<?php
declare( strict_types=1 );
namespace ExtCPTs;
use WP_Post;
use WP_Taxonomy;
class TaxonomyAdmin {
/**
* Default arguments for custom taxonomies.
*
* @var array<string,mixed>
*/
protected array $defaults = [
'meta_box' => null, # Custom arg
'dashboard_glance' => false, # Custom arg
'checked_ontop' => null, # Custom arg
'admin_cols' => null, # Custom arg
'required' => false, # Custom arg
];
public Taxonomy $taxo;
/**
* @var array<string,mixed>
*/
public array $args;
/**
* @var array<string,string>
*/
protected array $_cols;
/**
* @var array<string,string>
*/
protected ?array $the_cols = null;
/**
* Class constructor.
*
* @param Taxonomy $taxo An extended taxonomy object.
* @param array<string,mixed> $args Optional. The admin arguments.
*/
public function __construct( Taxonomy $taxo, array $args = [] ) {
$this->taxo = $taxo;
# Merge our args with the defaults:
$this->args = array_merge( $this->defaults, $args );
# Set checked on top to false unless we're using the default meta box:
if ( null === $this->args['checked_ontop'] ) {
$this->args['checked_ontop'] = empty( $this->args['meta_box'] );
}
}
/**
* Initialise the admin features of the taxonomy by adding the necessary actions and filters.
*/
public function init(): void {
# Meta boxes:
if ( $this->taxo->args['exclusive'] || isset( $this->args['meta_box'] ) ) {
add_action( 'add_meta_boxes', [ $this, 'meta_boxes' ], 10, 2 );
}
# 'At a Glance' dashboard panels:
if ( $this->args['dashboard_glance'] ) {
add_filter( 'dashboard_glance_items', [ $this, 'glance_items' ] );
}
# Term updated messages:
add_filter( 'term_updated_messages', [ $this, 'term_updated_messages' ], 1 );
# Admin columns:
if ( $this->args['admin_cols'] ) {
add_filter( "manage_edit-{$this->taxo->taxonomy}_columns", [ $this, '_log_default_cols' ], 0 );
add_filter( "manage_edit-{$this->taxo->taxonomy}_columns", [ $this, 'cols' ] );
add_filter( "manage_{$this->taxo->taxonomy}_custom_column", [ $this, 'col' ], 10, 3 );
}
/**
* Fired when the extended taxonomy admin instance is set up.
*
* @since 5.0.0
*
* @param \ExtCPTs\TaxonomyAdmin $instance The extended taxonomy admin instance.
*/
do_action( "ext-taxos/{$this->taxo->taxonomy}/admin-instance", $this );
}
/**
* Logs the default columns so we don't remove any custom columns added by other plugins.
*
* @param array<string,string> $cols The default columns for this taxonomy screen.
* @return array<string,string> The default columns for this taxonomy screen.
*/
public function _log_default_cols( array $cols ): array {
$this->_cols = $cols;
return $this->_cols;
}
/**
* Add columns to the admin screen for this taxonomy.
*
* Each item in the `admin_cols` array is either a string name of an existing column, or an associative
* array of information for a custom column.
*
* Defining a custom column is easy. Just define an array which includes the column title, column
* type, and optional callback function. You can display columns for term meta or custom functions.
*
* The example below adds two columns; one which displays the value of the term's `term_updated` meta
* key, and one which calls a custom callback function:
*
* register_extended_taxonomy( 'foo', 'bar', array(
* 'admin_cols' => array(
* 'foo_updated' => array(
* 'title' => 'Updated',
* 'meta_key' => 'term_updated'
* ),
* 'foo_bar' => array(
* 'title' => 'Example',
* 'function' => 'my_custom_callback'
* )
* )
* ) );
*
* That's all you need to do. The columns will handle safely outputting the data
* (escaping text, and comma-separating taxonomy terms). No more messing about with all of those
* annoyingly named column filters and actions.
*
* Each item in the `admin_cols` array must contain one of the following elements which defines the column type:
*
* - meta_key - A term meta key
* - function - The name of a callback function
*
* The value for the corresponding term meta are safely escaped and output into the column.
*
* There are a few optional elements:
*
* - title - Generated from the field if not specified.
* - function - The name of a callback function for the column (eg. `my_function`) which gets called
* instead of the built-in function for handling that column. The function is passed the term ID as
* its first parameter.
* - date_format - This is used with the `meta_key` column type. The value of the meta field will be
* treated as a timestamp if this is present. Unix and MySQL format timestamps are supported in the
* meta value. Pass in boolean true to format the date according to the 'Date Format' setting, or pass
* in a valid date formatting string (eg. `d/m/Y H:i:s`).
* - cap - A capability required in order for this column to be displayed to the current user. Defaults
* to null, meaning the column is shown to all users.
*
* Note that sortable admin columns are not yet supported.
*
* @param array<string,string> $cols Associative array of columns.
* @return array<string,string> Updated array of columns.
*/
public function cols( array $cols ): array {
// This function gets called multiple times, so let's cache it for efficiency:
if ( isset( $this->the_cols ) ) {
return $this->the_cols;
}
$new_cols = [];
$keep = [
'cb',
'name',
'description',
'slug',
];
# Add existing columns we want to keep:
foreach ( $cols as $id => $title ) {
if ( in_array( $id, $keep, true ) && ! isset( $this->args['admin_cols'][ $id ] ) ) {
$new_cols[ $id ] = $title;
}
}
/** @var array<string,(string|mixed[])> */
$admin_cols = array_filter( $this->args['admin_cols'] );
# Add our custom columns:
foreach ( $admin_cols as $id => $col ) {
if ( is_string( $col ) && isset( $cols[ $col ] ) ) {
# Existing (ie. built-in) column with id as the value
$new_cols[ $col ] = $cols[ $col ];
} elseif ( is_string( $col ) && isset( $cols[ $id ] ) ) {
# Existing (ie. built-in) column with id as the key and title as the value
$new_cols[ $id ] = esc_html( $col );
} elseif ( is_array( $col ) ) {
if ( isset( $col['cap'] ) && ! current_user_can( $col['cap'] ) ) {
continue;
}
if ( isset( $col['title_cb'] ) ) {
$new_cols[ $id ] = call_user_func( $col['title_cb'], $col );
} else {
$title = esc_html( $this->get_item_title( $col, $id ) );
if ( isset( $col['title_icon'] ) ) {
$title = sprintf(
'<span class="dashicons %s" aria-hidden="true"></span><span class="screen-reader-text">%s</span>',
esc_attr( $col['title_icon'] ),
$title
);
}
$new_cols[ $id ] = $title;
}
}
}
# Re-add any custom columns:
$custom = array_diff_key( $cols, $this->_cols );
$new_cols = array_merge( $new_cols, $custom );
$this->the_cols = $new_cols;
return $this->the_cols;
}
/**
* Output the column data for our custom columns.
*
* @param string $string Blank string.
* @param string $col Name of the column.
* @param int $term_id Term ID.
* @return string Blank string.
*/
public function col( string $string, string $col, int $term_id ): string {
# Shorthand:
$c = $this->args['admin_cols'];
# We're only interested in our custom columns:
$custom_cols = array_filter( array_keys( $c ) );
if ( ! in_array( $col, $custom_cols, true ) ) {
return $string;
}
if ( isset( $c[ $col ]['function'] ) ) {
call_user_func( $c[ $col ]['function'], $term_id );
} elseif ( isset( $c[ $col ]['meta_key'] ) ) {
$this->col_term_meta( $c[ $col ]['meta_key'], $c[ $col ], $term_id );
}
return $string;
}
/**
* Output column data for a term meta field.
*
* @param string $meta_key The term meta key.
* @param array<string,mixed> $args Array of arguments for this field.
* @param int $term_id Term ID.
*/
public function col_term_meta( string $meta_key, array $args, int $term_id ): void {
$vals = get_term_meta( $term_id, $meta_key, false );
$echo = [];
sort( $vals );
if ( isset( $args['date_format'] ) ) {
if ( true === $args['date_format'] ) {
$args['date_format'] = get_option( 'date_format' );
}
foreach ( $vals as $val ) {
if ( is_numeric( $val ) ) {
$echo[] = date( $args['date_format'], (int) $val );
} elseif ( ! empty( $val ) ) {
$echo[] = mysql2date( $args['date_format'], $val );
}
}
} else {
foreach ( $vals as $val ) {
if ( ! empty( $val ) || ( '0' === $val ) ) {
$echo[] = $val;
}
}
}
if ( empty( $echo ) ) {
echo '&#8212;';
} else {
echo esc_html( implode( ', ', $echo ) );
}
}
/**
* Returns a sensible title for the current item (usually the arguments array for a column).
*
* @param array<string,mixed> $item An array of arguments.
* @param string $fallback Fallback item title.
* @return string The item title.
*/
protected function get_item_title( array $item, string $fallback = '' ): string {
if ( isset( $item['title'] ) ) {
return $item['title'];
} elseif ( isset( $item['meta_key'] ) ) {
return ucwords( trim( str_replace( [ '_', '-' ], ' ', $item['meta_key'] ) ) );
}
return $fallback;
}
/**
* Removes the default meta box from the post editing screen and adds our custom meta box.
*
* @param string $object_type The object type (eg. the post type).
* @param mixed $object The object (eg. a WP_Post object).
*/
public function meta_boxes( string $object_type, $object ): void {
if ( ! is_a( $object, 'WP_Post' ) ) {
return;
}
$post_type = $object_type;
$post = $object;
$taxos = get_post_taxonomies( $post );
if ( in_array( $this->taxo->taxonomy, $taxos, true ) ) {
/** @var WP_Taxonomy */
$tax = get_taxonomy( $this->taxo->taxonomy );
# Remove default meta box from classic editor:
if ( $this->taxo->args['hierarchical'] ) {
remove_meta_box( "{$this->taxo->taxonomy}div", $post_type, 'side' );
} else {
remove_meta_box( "tagsdiv-{$this->taxo->taxonomy}", $post_type, 'side' );
}
$store = version_compare( $GLOBALS['wp_version'], '6.5', '>=' ) ? 'core/editor' : 'core/edit-post';
# Remove default meta box from block editor:
wp_add_inline_script(
'wp-edit-post',
sprintf(
'wp.data.dispatch( "%s" ).removeEditorPanel( "taxonomy-panel-%s" );',
$store,
$this->taxo->taxonomy
)
);
if ( ! current_user_can( $tax->cap->assign_terms ) ) {
return;
}
if ( $this->args['meta_box'] ) {
# Set the 'meta_box' argument to the actual meta box callback function name:
if ( 'simple' === $this->args['meta_box'] ) {
if ( $this->taxo->args['exclusive'] ) {
$this->args['meta_box'] = [ $this, 'meta_box_radio' ];
} else {
$this->args['meta_box'] = [ $this, 'meta_box_simple' ];
}
} elseif ( 'radio' === $this->args['meta_box'] ) {
$this->taxo->args['exclusive'] = true;
$this->args['meta_box'] = [ $this, 'meta_box_radio' ];
} elseif ( 'dropdown' === $this->args['meta_box'] ) {
$this->taxo->args['exclusive'] = true;
$this->args['meta_box'] = [ $this, 'meta_box_dropdown' ];
}
# Add the meta box, using the plural or singular taxonomy label where relevant:
if ( $this->taxo->args['exclusive'] ) {
add_meta_box( "{$this->taxo->taxonomy}div", $tax->labels->singular_name, $this->args['meta_box'], $post_type, 'side' );
} else {
add_meta_box( "{$this->taxo->taxonomy}div", $tax->labels->name, $this->args['meta_box'], $post_type, 'side' );
}
} elseif ( false !== $this->args['meta_box'] ) {
# This must be an 'exclusive' taxonomy. Add the radio meta box:
add_meta_box( "{$this->taxo->taxonomy}div", $tax->labels->singular_name, [ $this, 'meta_box_radio' ], $post_type, 'side' );
}
}
}
/**
* Displays the 'radio' meta box on the post editing screen.
*
* Uses the Walker\Radios class for the walker.
*
* @param WP_Post $post The post object.
* @param array<string,mixed> $meta_box The meta box arguments.
*/
public function meta_box_radio( WP_Post $post, array $meta_box ): void {
$walker = new Walker\Radios();
$this->do_meta_box( $post, $walker, true, 'checklist' );
}
/**
* Displays the 'dropdown' meta box on the post editing screen.
*
* Uses the Walker\Dropdown class for the walker.
*
* @param WP_Post $post The post object.
* @param array<string,mixed> $meta_box The meta box arguments.
*/
public function meta_box_dropdown( WP_Post $post, array $meta_box ): void {
$walker = new Walker\Dropdown();
$this->do_meta_box( $post, $walker, true, 'dropdown' );
}
/**
* Displays the 'simple' meta box on the post editing screen.
*
* @param WP_Post $post The post object.
* @param array<string,mixed> $meta_box The meta box arguments.
*/
public function meta_box_simple( WP_Post $post, array $meta_box ): void {
$this->do_meta_box( $post );
}
/**
* Displays a meta box on the post editing screen.
*
* @param WP_Post $post The post object.
* @param \Walker $walker Optional. A term walker.
* @param bool $show_none Optional. Whether to include a 'none' item in the term list. Default false.
* @param string $type Optional. The taxonomy list type (checklist or dropdown). Default 'checklist'.
*/
protected function do_meta_box( WP_Post $post, ?\Walker $walker = null, bool $show_none = false, string $type = 'checklist' ): void {
$taxonomy = $this->taxo->taxonomy;
/** @var WP_Taxonomy */
$tax = get_taxonomy( $taxonomy );
/** @var array<int,int> */
$selected = wp_get_object_terms(
$post->ID,
$taxonomy,
[
'fields' => 'ids',
]
);
if ( $show_none ) {
if ( isset( $tax->labels->no_item ) ) {
/** @var string $none */
$none = $tax->labels->no_item;
} else {
$none = esc_html__( 'Not specified', 'extended-cpts' );
}
} else {
$none = '';
}
/**
* Execute code before the taxonomy meta box content outputs to the page.
*
* @since 2.0.0
*
* @param WP_Taxonomy $tax The current taxonomy object.
* @param WP_Post $post The current post object.
* @param string $type The taxonomy list type ('checklist' or 'dropdown').
*/
do_action( 'ext-taxos/meta_box/before', $tax, $post, $type );
?>
<div id="taxonomy-<?php echo esc_attr( $taxonomy ); ?>" class="categorydiv">
<?php
switch ( $type ) {
case 'dropdown':
printf(
'<label for="%1$s" class="screen-reader-text">%2$s</label>',
esc_attr( "{$taxonomy}dropdown" ),
esc_html( $tax->labels->singular_name )
);
$dropdown_args = [
'option_none_value' => ( is_taxonomy_hierarchical( $taxonomy ) ? '-1' : '' ),
'show_option_none' => $none,
'hide_empty' => false,
'hierarchical' => true,
'show_count' => false,
'orderby' => 'name',
'selected' => reset( $selected ) ?: 0,
'id' => "{$taxonomy}dropdown",
'name' => is_taxonomy_hierarchical( $taxonomy ) ? "tax_input[{$taxonomy}][]" : "tax_input[{$taxonomy}]",
'taxonomy' => $taxonomy,
'required' => $this->args['required'],
];
if ( $walker instanceof \Walker ) {
$dropdown_args['walker'] = $walker;
}
wp_dropdown_categories( $dropdown_args );
break;
case 'checklist':
default:
?>
<style type="text/css">
/* Style for the 'none' item: */
#<?php echo esc_attr( $taxonomy ); ?>-0 {
color: #888;
border-top: 1px solid #eee;
margin-top: 5px;
padding-top: 5px;
}
</style>
<input type="hidden" name="tax_input[<?php echo esc_attr( $taxonomy ); ?>][]" value="0" />
<ul id="<?php echo esc_attr( $taxonomy ); ?>checklist" class="list:<?php echo esc_attr( $taxonomy ); ?> categorychecklist form-no-clear">
<?php
# Standard WP Walker_Category_Checklist does not cut it
if ( ! $walker ) {
$walker = new Walker\Checkboxes();
}
# Output the terms:
wp_terms_checklist(
$post->ID,
[
'taxonomy' => $taxonomy,
'walker' => $walker,
'selected_cats' => $selected,
'checked_ontop' => $this->args['checked_ontop'],
]
);
# Output the 'none' item:
if ( $show_none ) {
$output = '';
$o = (object) [
'term_id' => 0,
'name' => $none,
'slug' => 'none',
];
if ( empty( $selected ) ) {
$_selected = [ 0 ];
} else {
$_selected = $selected;
}
$args = [
'taxonomy' => $taxonomy,
'selected_cats' => $_selected,
'disabled' => false,
];
$walker->start_el( $output, $o, 1, $args );
$walker->end_el( $output, $o, 1, $args );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $output;
}
?>
</ul>
<?php
break;
}
?>
</div>
<?php
/**
* Execute code after the taxonomy meta box content outputs to the page.
*
* @since 2.0.0
*
* @param WP_Taxonomy $tax The current taxonomy object.
* @param WP_Post $post The current post object.
* @param string $type The taxonomy list type ('checklist' or 'dropdown').
*/
do_action( 'ext-taxos/meta_box/after', $tax, $post, $type );
}
/**
* Adds our taxonomy to the 'At a Glance' widget on the dashboard.
*
* @param array<int,string> $items Array of items to display on the widget.
* @return array<int,string> Updated array of items.
*/
public function glance_items( array $items ): array {
/** @var WP_Taxonomy */
$taxonomy = get_taxonomy( $this->taxo->taxonomy );
if ( ! current_user_can( $taxonomy->cap->manage_terms ) ) {
return $items;
}
if ( $taxonomy->_builtin ) {
return $items;
}
# Get the labels and format the counts:
$count = wp_count_terms(
[
'taxonomy' => $this->taxo->taxonomy,
]
);
if ( is_wp_error( $count ) ) {
return $items;
}
$text = self::n( $taxonomy->labels->singular_name, $taxonomy->labels->name, (int) $count );
$num = number_format_i18n( (int) $count );
# This is absolutely not localisable. WordPress 3.8 didn't add a new taxonomy label.
$url = add_query_arg(
[
'taxonomy' => $this->taxo->taxonomy,
'post_type' => reset( $taxonomy->object_type ),
],
admin_url( 'edit-tags.php' )
);
$text = '<a href="' . esc_url( $url ) . '" class="taxo-' . esc_attr( $this->taxo->taxonomy ) . '-count">' . esc_html( $num . ' ' . $text ) . '</a>';
# Go!
$items[] = $text;
return $items;
}
/**
* Adds our term updated messages.
*
* The messages are as follows:
*
* 1 => "Term added."
* 2 => "Term deleted."
* 3 => "Term updated."
* 4 => "Term not added."
* 5 => "Term not updated."
* 6 => "Terms deleted."
*
* @param array<string, array<int, string>> $messages An array of term updated message arrays keyed by taxonomy name.
* @return array<string, array<int, string>> Updated array of term updated messages.
*/
public function term_updated_messages( array $messages ): array {
$messages[ $this->taxo->taxonomy ] = [
1 => esc_html( sprintf( '%s added.', $this->taxo->tax_singular ) ),
2 => esc_html( sprintf( '%s deleted.', $this->taxo->tax_singular ) ),
3 => esc_html( sprintf( '%s updated.', $this->taxo->tax_singular ) ),
4 => esc_html( sprintf( '%s not added.', $this->taxo->tax_singular ) ),
5 => esc_html( sprintf( '%s not updated.', $this->taxo->tax_singular ) ),
6 => esc_html( sprintf( '%s deleted.', $this->taxo->tax_plural ) ),
];
return $messages;
}
/**
* A non-localised version of _n()
*
* @param string $single The text that will be used if $number is 1.
* @param string $plural The text that will be used if $number is not 1.
* @param int $number The number to compare against to use either $single or $plural.
* @return string Either $single or $plural text.
*/
public static function n( string $single, string $plural, int $number ): string {
return ( 1 === intval( $number ) ) ? $single : $plural;
}
}