Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/js/_enqueues/admin/inline-edit-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,19 @@ window.wp = window.wp || {};
}
});

/**
* Preselects the current term for single-term taxonomies, which render
* radio buttons (hierarchical) or a dropdown (non-hierarchical).
* A value of 0 selects the "None" option when one is offered.
*/
$( '.post_single_term', rowData ).each( function() {
var taxname = $( this ).attr( 'id' ).replace( '_' + id, '' ),
termId = $( this ).text().trim() || '0';

$( 'ul.' + taxname + '-checklist :radio', editRow ).val( [ termId ] );
$( 'select[name="tax_input[' + taxname + '][]"]', editRow ).val( termId );
});

/**
* Gets all the taxonomies for live auto-fill suggestions when typing the name
* of a tag.
Expand Down
84 changes: 77 additions & 7 deletions src/wp-admin/includes/class-wp-posts-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1900,10 +1900,56 @@
<?php foreach ( $hierarchical_taxonomies as $taxonomy ) : ?>

<span class="title inline-edit-categories-label"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
<input type="hidden" name="<?php echo ( 'category' === $taxonomy->name ) ? 'post_category[]' : 'tax_input[' . esc_attr( $taxonomy->name ) . '][]'; ?>" value="0" />
<ul class="cat-checklist <?php echo esc_attr( $taxonomy->name ); ?>-checklist">
<?php wp_terms_checklist( 0, array( 'taxonomy' => $taxonomy->name ) ); ?>
</ul>
<?php
$tax_field_name = ( 'category' === $taxonomy->name )
? 'post_category[]'
: 'tax_input[' . esc_attr( $taxonomy->name ) . '][]';

$single_term = isset( $taxonomy->allow_multiple_terms ) && ! $taxonomy->allow_multiple_terms;
?>
<input type="hidden" name="<?php echo $tax_field_name; ?>" value="0" />
<?php if ( $single_term ) : ?>
<ul class="cat-checklist <?php echo esc_attr( $taxonomy->name ); ?>-checklist single-term-checklist">
<?php
$terms = get_terms(
array(
'taxonomy' => $taxonomy->name,
'hide_empty' => false,
)
);
if ( ! is_wp_error( $terms ) ) :
$has_default_term = ! empty( $taxonomy->default_term );
if ( ! $has_default_term ) :
?>
<li>
<label>
<input type="radio" name="<?php echo $tax_field_name; ?>" value="0" />
<?php
/* translators: Option shown when no single-term value is selected. */
esc_html_e( 'None' );
?>
</label>
</li>
<?php
endif;
foreach ( $terms as $term ) :
?>
<li>
<label>
<input type="radio" name="<?php echo $tax_field_name; ?>" value="<?php echo (int) $term->term_id; ?>" />
<?php echo esc_html( $term->name ); ?>
</label>
</li>
<?php
endforeach;
endif;
?>
</ul>
<?php else : ?>
<ul class="cat-checklist <?php echo esc_attr( $taxonomy->name ); ?>-checklist">
<?php wp_terms_checklist( 0, array( 'taxonomy' => $taxonomy->name ) ); ?>
</ul>
<?php endif; ?>

<?php endforeach; // $hierarchical_taxonomies as $taxonomy ?>

Expand Down Expand Up @@ -1996,13 +2042,37 @@
<?php foreach ( $flat_taxonomies as $taxonomy ) : ?>

<?php if ( current_user_can( $taxonomy->cap->assign_terms ) ) : ?>
<?php $taxonomy_name = esc_attr( $taxonomy->name ); ?>
<?php
$taxonomy_name = esc_attr( $taxonomy->name );
$single_term = isset( $taxonomy->allow_multiple_terms ) && ! $taxonomy->allow_multiple_terms;
?>
<div class="inline-edit-tags-wrap">
<label class="inline-edit-tags">
<span class="title"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
<textarea data-wp-taxonomy="<?php echo $taxonomy_name; ?>" cols="22" rows="1" name="tax_input[<?php echo esc_attr( $taxonomy->name ); ?>]" class="tax_input_<?php echo esc_attr( $taxonomy->name ); ?>" aria-describedby="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"></textarea>
<?php if ( $single_term ) : ?>
<?php
$has_default_term = ! empty( $taxonomy->default_term );
wp_dropdown_categories(
array(

Check warning on line 2056 in src/wp-admin/includes/class-wp-posts-list-table.php

View workflow job for this annotation

GitHub Actions / PHP static analysis / Run PHP static analysis

Parameter #1 $args of function wp_dropdown_categories expects array{show_option_all?: string, show_option_none?: string, option_none_value?: string, orderby?: string, pad_counts?: bool, show_count?: bool|int, echo?: bool|int, hierarchical?: bool|int, ..., ...}|string, array{taxonomy: string, hide_empty: false, hierarchical: false, orderby: 'name', name: non-falsy-string, id: non-falsy-string, show_option_none: string, option_none_value: 0, ...} given.
'taxonomy' => $taxonomy->name,
'hide_empty' => false,
'hierarchical' => false,
'orderby' => 'name',
'name' => 'tax_input[' . $taxonomy->name . '][]',
'id' => 'inline-edit-' . $taxonomy->name . '-select',
'show_option_none' => $has_default_term ? '' : __( 'None' ),
'option_none_value' => 0,
'value_field' => 'term_id',
)
);
?>
<?php else : ?>
<textarea data-wp-taxonomy="<?php echo $taxonomy_name; ?>" cols="22" rows="1" name="tax_input[<?php echo esc_attr( $taxonomy->name ); ?>]" class="tax_input_<?php echo esc_attr( $taxonomy->name ); ?>" aria-describedby="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"></textarea>
<?php endif; ?>
</label>
<p class="howto" id="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"><?php echo esc_html( $taxonomy->labels->separate_items_with_commas ); ?></p>
<?php if ( ! $single_term ) : ?>
<p class="howto" id="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"><?php echo esc_html( $taxonomy->labels->separate_items_with_commas ); ?></p>
<?php endif; ?>
</div>
<?php endif; // current_user_can( 'assign_terms' ) ?>

Expand Down
120 changes: 120 additions & 0 deletions src/wp-admin/includes/meta-boxes.php
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,126 @@
endif;
}

/**
* Displays a single-term taxonomy meta box.
*
* Used as the default meta box callback for taxonomies registered with
* `allow_multiple_terms` set to false. Renders radio buttons for hierarchical
* taxonomies and a dropdown for non-hierarchical ones, so that at most one
* term can be selected.
*
* @since 7.2.0
*
* @param WP_Post $post Current post object.
* @param array $box {
* Single-term meta box arguments.
*
* @type string $id Meta box 'id' attribute.
* @type string $title Meta box title.
* @type callable $callback Meta box display callback.
* @type array $args {
* Extra meta box arguments.
*
* @type string $taxonomy Taxonomy name.
* }
* }
*/
function post_single_term_meta_box( $post, $box ) {
$defaults = array( 'taxonomy' => 'category' );
if ( ! isset( $box['args'] ) || ! is_array( $box['args'] ) ) {
$args = array();
} else {
$args = $box['args'];
}
$parsed_args = wp_parse_args( $args, $defaults );
$tax_name = esc_attr( $parsed_args['taxonomy'] );
$taxonomy = get_taxonomy( $parsed_args['taxonomy'] );

if ( ! $taxonomy ) {
return;
}

// Input field name matches what the core save path expects.
$name = ( 'category' === $tax_name ) ? 'post_category' : 'tax_input[' . $tax_name . ']';

// Current term for this post, if any.
$current_terms = get_the_terms( $post->ID, $tax_name );
$current_id = 0;
if ( is_array( $current_terms ) && ! empty( $current_terms ) ) {
$current_term = reset( $current_terms );
$current_id = (int) $current_term->term_id;
}

// A "None" option is only offered when no default term is configured.
$has_default_term = ! empty( $taxonomy->default_term );

?>
<div id="taxonomy-<?php echo $tax_name; ?>" class="single-term-taxonomy-div">
<?php
// Always submit a sentinel value so an empty set is sent when nothing is chosen.
echo '<input type="hidden" name="' . esc_attr( $name ) . '[]" value="0" />';

if ( $taxonomy->hierarchical ) :
$terms = get_terms(
array(
'taxonomy' => $tax_name,
'hide_empty' => false,
)
);

if ( is_wp_error( $terms ) || empty( $terms ) ) {
echo '<p>' . esc_html( $taxonomy->labels->not_found ) . '</p>';
} else {
?>
<fieldset>
<legend class="screen-reader-text"><?php echo esc_html( $taxonomy->labels->name ); ?></legend>
<ul class="single-term-taxonomy-radiolist">
<?php if ( ! $has_default_term ) : ?>
<li>
<label>
<input type="radio" name="<?php echo esc_attr( $name ); ?>[]" value="0" <?php checked( 0, $current_id ); ?> />
<?php
/* translators: Option shown when no single-term value is selected. */
esc_html_e( 'None' );
?>
</label>
</li>
<?php endif; ?>
<?php foreach ( $terms as $term ) : ?>
<li>
<label>
<input type="radio" name="<?php echo esc_attr( $name ); ?>[]" value="<?php echo (int) $term->term_id; ?>" <?php checked( (int) $term->term_id, $current_id ); ?> />
<?php echo esc_html( $term->name ); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</fieldset>
<?php
}
else :
$dropdown_args = array(
'taxonomy' => $tax_name,
'hide_empty' => false,
'hierarchical' => false,
'orderby' => 'name',
'name' => $name . '[]',
'id' => 'taxonomy-' . $tax_name . '-select',
'selected' => $current_id,
'show_option_none' => $has_default_term ? '' : __( 'None' ),
'option_none_value' => 0,
'value_field' => 'term_id',
);
?>
<label for="taxonomy-<?php echo $tax_name; ?>-select" class="screen-reader-text"><?php echo esc_html( $taxonomy->labels->name ); ?></label>
<?php wp_dropdown_categories( $dropdown_args ); ?>

Check warning on line 663 in src/wp-admin/includes/meta-boxes.php

View workflow job for this annotation

GitHub Actions / PHP static analysis / Run PHP static analysis

Parameter #1 $args of function wp_dropdown_categories expects array{show_option_all?: string, show_option_none?: string, option_none_value?: string, orderby?: string, pad_counts?: bool, show_count?: bool|int, echo?: bool|int, hierarchical?: bool|int, ..., ...}|string, array{taxonomy: string, hide_empty: false, hierarchical: false, orderby: 'name', name: non-falsy-string, id: non-falsy-string, selected: int, show_option_none: string, ...} given.
<?php
endif;
?>
</div>
<?php
}

/**
* Displays post tags form fields.
*
Expand Down
34 changes: 34 additions & 0 deletions src/wp-admin/includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,40 @@ function taxonomy_meta_box_sanitize_cb_checkboxes( $taxonomy, $terms ) {
return array_map( 'intval', $terms );
}

/**
* Sanitizes POST values from a single-term taxonomy metabox.
*
* Accepts the raw array submitted by {@see post_single_term_meta_box()} (radio buttons
* or a single-select dropdown), discards the `0` sentinel/"None" value, and returns
* at most one term ID.
*
* @since 7.2.0
*
* @param string $taxonomy The taxonomy name.
* @param mixed $terms Raw term data from the 'tax_input' or 'post_category' field.
* @return int[] Array containing a single sanitized term ID, or an empty array.
*/
function taxonomy_meta_box_sanitize_cb_single_term( $taxonomy, $terms ) {
if ( ! is_array( $terms ) ) {
$terms = array( $terms );
}

$clean = array();
foreach ( $terms as $term ) {
$term = (int) $term;
if ( $term > 0 ) {
$clean[] = $term;
}
}

if ( empty( $clean ) ) {
return array();
}

// Keep only the last submitted term to honor the single-term constraint.
return array( end( $clean ) );
}

/**
* Sanitizes POST values from an input taxonomy metabox.
*
Expand Down
14 changes: 13 additions & 1 deletion src/wp-admin/includes/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,19 @@ function get_inline_data( $post ) {
continue;
}

if ( $taxonomy->hierarchical ) {
if ( ! $taxonomy->allow_multiple_terms ) {

/*
* Single-term taxonomies render radio buttons (hierarchical) or a dropdown
* (non-hierarchical) in Quick Edit, both keyed by term ID, so expose the
* current term ID for inline-edit-post.js to preselect.
*/
$terms = get_the_terms( $post->ID, $taxonomy_name );
$term_id = ( is_array( $terms ) && ! empty( $terms ) ) ? (int) reset( $terms )->term_id : 0;

echo '<div class="post_single_term" id="' . $taxonomy_name . '_' . $post->ID . '">' . $term_id . '</div>';

} elseif ( $taxonomy->hierarchical ) {

$terms = get_object_term_cache( $post->ID, $taxonomy_name );
if ( false === $terms ) {
Expand Down
22 changes: 21 additions & 1 deletion src/wp-includes/class-wp-taxonomy.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ final class WP_Taxonomy {
*/
public $show_admin_column = false;

/**
* Whether objects of this taxonomy can be associated with more than one term.
*
* When set to false, core admin UIs render a single-select control (radio buttons for
* hierarchical taxonomies, a dropdown for non-hierarchical ones) and term assignment
* is capped at one term per object across all entry points, including the REST API
* and wp_set_object_terms(). Default true.
*
* @since 7.2.0
* @var bool
*/
public $allow_multiple_terms = true;

/**
* The callback function for the meta box display.
*
Expand Down Expand Up @@ -357,6 +370,7 @@ public function set_props( $object_type, $args ) {
'show_tagcloud' => null,
'show_in_quick_edit' => null,
'show_admin_column' => false,
'allow_multiple_terms' => true,
'meta_box_cb' => null,
'meta_box_sanitize_cb' => null,
'capabilities' => array(),
Expand Down Expand Up @@ -440,7 +454,9 @@ public function set_props( $object_type, $args ) {

// If not set, use the default meta box.
if ( null === $args['meta_box_cb'] ) {
if ( $args['hierarchical'] ) {
if ( ! $args['allow_multiple_terms'] ) {
$args['meta_box_cb'] = 'post_single_term_meta_box';
} elseif ( $args['hierarchical'] ) {
$args['meta_box_cb'] = 'post_categories_meta_box';
} else {
$args['meta_box_cb'] = 'post_tags_meta_box';
Expand All @@ -452,6 +468,10 @@ public function set_props( $object_type, $args ) {
// Default meta box sanitization callback depends on the value of 'meta_box_cb'.
if ( null === $args['meta_box_sanitize_cb'] ) {
switch ( $args['meta_box_cb'] ) {
case 'post_single_term_meta_box':
$args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_single_term';
break;

case 'post_categories_meta_box':
$args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_checkboxes';
break;
Expand Down
Loading
Loading